diff --git a/client/src/app-components/people-list.js b/client/src/app-components/people-list.js new file mode 100644 index 00000000..604d8967 --- /dev/null +++ b/client/src/app-components/people-list.js @@ -0,0 +1,117 @@ +import React from 'react'; +import _ from 'lodash'; +import {StaggeredMotion, spring} from 'react-motion'; +import Menu from 'core-components/menu' + +import DateTransformer from 'lib-core/date-transformer'; +import i18n from 'lib-app/i18n'; + +class PeopleList extends React.Component { + static propTypes = { + list: React.PropTypes.arrayOf(React.PropTypes.shape({ + profilePic: React.PropTypes.string, + name: React.PropTypes.string, + assignedTickets: React.PropTypes.number, + closedTickets: React.PropTypes.number, + lastLogin: React.PropTypes.number + })), + pageSize: React.PropTypes.number, + page: React.PropTypes.number, + onPageSelect: React.PropTypes.func + }; + + static defaultProps = { + pageSize: 4, + list: [] + }; + + render() { + const pages = _.range(1, this.getPages() + 1).map((index) => {return {content: index};}); + + return ( +
+
+ + {this.renderList.bind(this)} + +
+
+ +
+
+ ); + } + + getDefaultStyles() { + return _.times(this.props.pageSize).map(() => {return {offset: -100, alpha: 0}}); + } + + getStyles(prevStyles) { + return prevStyles.map((_, i) => { + return i === 0 + ? {offset: spring(0), alpha: spring(1)} + : {offset: spring(prevStyles[i - 1].offset), alpha: spring(prevStyles[i - 1].alpha)} + }); + } + + renderList(styles) { + return ( +
+ {styles.map(this.renderAnimatedItem.bind(this))} +
+ ); + } + + renderAnimatedItem(style, index) { + return ( +
+ {this.renderItem(index + this.props.pageSize * (this.props.page - 1))} +
+ ); + } + + renderItem(index) { + if(index >= this.props.list.length) { + return null; + } + + const item = this.props.list[index]; + const minIndex = this.props.pageSize * (this.props.page - 1); + const maxIndex = this.props.pageSize * this.props.page; + + return (minIndex <= index && index < maxIndex) ? ( +
+
+ +
+
{item.name}
+
+ {i18n('ASSIGNED_TICKETS', {tickets: item.assignedTickets})} +
+
+ {i18n('CLOSED_TICKETS', {tickets: item.closedTickets})} +
+
+
{i18n('LAST_LOGIN')}
+
{DateTransformer.transformToString(item.lastLogin)}
+
+
+ ) : null; + } + + getRowsQuantity() { + if(this.props.page == this.getPages()){ + return this.props.list.length % this.props.pageSize; + } + else { + return this.props.pageSize; + } + } + + getPages() { + return Math.ceil(this.props.list.length / this.props.pageSize); + } + +} + +export default PeopleList; \ No newline at end of file diff --git a/client/src/app-components/people-list.scss b/client/src/app-components/people-list.scss new file mode 100644 index 00000000..8c52b03d --- /dev/null +++ b/client/src/app-components/people-list.scss @@ -0,0 +1,64 @@ +@import "../scss/vars"; + +.people-list { + max-width: 800px; + margin: 0 auto; + + &__list { + } + + &__item { + border: 2px solid $grey; + border-radius: 4px; + margin-bottom: 12px; + position: relative; + height: 105px; + padding-left: 60px; + font-size: $font-size--md; + + &-profile-pic-wrapper { + vertical-align: top; + background-color: $secondary-blue; + color: white; + border-radius: 5px; + width: 60px; + height: 60px; + top: 20px; + left: 20px; + + overflow: hidden; + position: absolute; + border: 2px solid $light-grey; + } + + &-profile-pic { + position: absolute; + height: 100%; + left: 50%; + transform: translate(-50%, 0) + } + + &-block { + padding: 30px 0; + width: 25%; + display: inline-block; + vertical-align: middle; + } + + &-assigned-tickets { + + } + + &-closed-tickets { + + } + + &-last-login { + + } + } + + &__pagination { + + } +} \ No newline at end of file diff --git a/client/src/app/admin/panel/staff/add-staff-modal.js b/client/src/app/admin/panel/staff/add-staff-modal.js new file mode 100644 index 00000000..defd85bd --- /dev/null +++ b/client/src/app/admin/panel/staff/add-staff-modal.js @@ -0,0 +1,105 @@ +import React from 'react'; +import _ from 'lodash'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import SessionStore from 'lib-app/session-store'; + +import Header from 'core-components/header' +import Form from 'core-components/form'; +import FormField from 'core-components/form-field'; +import SubmitButton from 'core-components/submit-button'; +import Button from 'core-components/button'; + +class AddStaffModal extends React.Component { + + static contextTypes = { + closeModal: React.PropTypes.func + }; + + state = { + loading: false, + errors: {}, + error: null + }; + + render() { + return ( +
+
+
this.setState({errors})} loading={this.state.loading}> +
+
+ + + +
+ +
+
+
+
+
{i18n('Departments')}
+ +
+
+
+ + {i18n('SAVE')} + + +
+
+ ); + } + + getDepartments() { + return SessionStore.getDepartments().map(department => department.name); + } + + onSubmit(form) { + let departments = _.filter(SessionStore.getDepartments(), (department, index) => { + return _.includes(form.departments, index); + }).map(department => department.id); + + this.setState({loading: true}); + + API.call({ + path: '/staff/add', + data: { + name: form.name, + email: form.email, + password: form.password, + level: form.level + 1, + department: JSON.stringify(departments) + } + }).then(this.context.closeModal).catch((result) => { + this.setState({ + loading: false, + error: result.message + }); + }); + } + + onCancelClick(event) { + event.preventDefault(); + this.context.closeModal(); + } + + getErrors() { + let errors = _.extend({}, this.state.errors); + + if (this.state.error === 'ALREADY_A_STAFF') { + errors.email = i18n('EMAIL_EXISTS'); + } + + return errors; + } +} + +export default AddStaffModal; \ No newline at end of file diff --git a/client/src/app/admin/panel/staff/add-staff-modal.scss b/client/src/app/admin/panel/staff/add-staff-modal.scss new file mode 100644 index 00000000..a2dd0d64 --- /dev/null +++ b/client/src/app/admin/panel/staff/add-staff-modal.scss @@ -0,0 +1,23 @@ +@import "../../../../scss/vars"; + +.add-staff-modal { + width: 700px; + + &__level-selector { + text-align: center; + } + + &__departments { + @include scrollbars(); + + border: 1px solid $grey; + padding: 20px; + height: 320px; + overflow-y: auto; + } + + &__departments-title { + font-size: $font-size--md; + text-align: center; + } +} \ No newline at end of file diff --git a/client/src/app/admin/panel/staff/admin-panel-staff-members.js b/client/src/app/admin/panel/staff/admin-panel-staff-members.js index f869fc8b..5d85a0ea 100644 --- a/client/src/app/admin/panel/staff/admin-panel-staff-members.js +++ b/client/src/app/admin/panel/staff/admin-panel-staff-members.js @@ -1,14 +1,99 @@ import React from 'react'; +import _ from 'lodash'; + +import i18n from 'lib-app/i18n'; +import API from 'lib-app/api-call'; +import SessionStore from 'lib-app/session-store'; +import PeopleList from 'app-components/people-list'; +import ModalContainer from 'app-components/modal-container'; + +import AddStaffModal from 'app/admin/panel/staff/add-staff-modal'; + +import Header from 'core-components/header'; +import DropDown from 'core-components/drop-down'; +import Button from 'core-components/button'; +import Icon from 'core-components/icon'; +import Loading from 'core-components/loading'; class AdminPanelStaffMembers extends React.Component { + state = { + selectedDepartment: 0, + staffList: [], + loading: true, + page: 1 + }; + + componentDidMount() { + API.call({ + path: '/staff/get-all', + data: {} + }).then(this.onStaffRetrieved.bind(this)); + } + render() { return ( -
- /admin/panel/staff/staff-members +
+
+
+ + +
+ {(this.state.loading) ? : this.setState({page: index+1})} />}
); } + + onAddNewStaff() { + ModalContainer.openModal(); + } + + getDepartmentDropdownProps() { + return { + items: this.getDepartments(), + onChange: (event) => { + let departments = SessionStore.getDepartments(); + this.setState({ + selectedDepartment: event.index && departments[event.index - 1].id, + page: 1 + }); + }, + size: 'medium' + }; + } + + getStaffList() { + if(!this.state.selectedDepartment) { + return this.state.staffList; + } + + return _.filter(this.state.staffList, (o) => { + return _.findIndex(o.departments, {id: this.state.selectedDepartment}) !== -1; + }); + } + + getDepartments() { + let departments = SessionStore.getDepartments().map((department) => { + return {content: department.name}; + }); + + departments.unshift({ + content: i18n('ALL_DEPARTMENTS') + }); + + return departments; + } + + onStaffRetrieved(result) { + if(result.status == 'success'){ + this.setState({ + loading: false, + staffList: result.data + }); + } + } } export default AdminPanelStaffMembers; \ No newline at end of file diff --git a/client/src/app/admin/panel/staff/admin-panel-staff-members.scss b/client/src/app/admin/panel/staff/admin-panel-staff-members.scss new file mode 100644 index 00000000..d31be626 --- /dev/null +++ b/client/src/app/admin/panel/staff/admin-panel-staff-members.scss @@ -0,0 +1,17 @@ +@import "../../../../scss/vars"; + +.admin-panel-staff-members { + + &__wrapper { + height: 60px; + } + + &__dropdown { + float: left; + } + + &__button { + float: right; + margin-top: -5px; + } +} \ No newline at end of file diff --git a/client/src/core-components/checkbox-group.js b/client/src/core-components/checkbox-group.js new file mode 100644 index 00000000..a5e97e76 --- /dev/null +++ b/client/src/core-components/checkbox-group.js @@ -0,0 +1,57 @@ +import React from 'react'; +import _ from 'lodash'; + +import Checkbox from 'core-components/checkbox'; + +class CheckboxGroup extends React.Component { + static propTypes = { + items: React.PropTypes.array.isRequired, + value: React.PropTypes.arrayOf(React.PropTypes.number), + onChange: React.PropTypes.func + }; + + state = { + value: [] + }; + + render() { + return ( + + ); + } + + renderItem(label, index) { + const checked = (_.includes(this.getValue(), index)); + + return ( +
  • + +
  • + ); + } + + onCheckboxChange(index) { + let value = _.clone(this.getValue()); + + if(_.includes(value, index)) { + _.pull(value, index); + } else { + value.push(index); + value.sort(); + } + + this.setState({value}); + + if(this.props.onChange) { + this.props.onChange({target: {value}}); + } + } + + getValue() { + return (this.props.value !== undefined) ? this.props.value : this.state.value; + } +} + +export default CheckboxGroup; \ No newline at end of file diff --git a/client/src/core-components/checkbox-group.scss b/client/src/core-components/checkbox-group.scss new file mode 100644 index 00000000..92612c5c --- /dev/null +++ b/client/src/core-components/checkbox-group.scss @@ -0,0 +1,11 @@ +@import "../scss/vars"; + +.checkbox-group { + list-style-type: none; + margin: 0; + padding: 0; + + &__item { + margin: 10px 0; + } +} \ No newline at end of file diff --git a/client/src/core-components/checkbox.js b/client/src/core-components/checkbox.js index c148783a..0dc5efed 100644 --- a/client/src/core-components/checkbox.js +++ b/client/src/core-components/checkbox.js @@ -11,28 +11,38 @@ class CheckBox extends React.Component { static propTypes = { alignment: React.PropTypes.string, label: React.PropTypes.string, - value: React.PropTypes.bool + value: React.PropTypes.bool, + wrapInLabel: React.PropTypes.bool, + onChange: React.PropTypes.func }; static defaultProps = { + wrapInLabel: false, alignment: 'right' }; - constructor(props) { - super(props); - - this.state = { - checked: false - }; - } + state = { + checked: false + }; render() { + let Wrapper = (this.props.wrapInLabel) ? 'label' : 'span'; + return ( - + {getIcon((this.getValue()) ? 'check-square' : 'square', 'lg') } + {(this.props.label) ? this.renderLabel() : null} + + ); + } + + renderLabel() { + return ( + + {this.props.label} ); } @@ -43,7 +53,7 @@ class CheckBox extends React.Component { props.type = 'checkbox'; props['aria-hidden'] = true; - props.className = 'checkbox--box'; + props.className = 'checkbox__box'; props.checked = this.getValue(); props.onChange = callback(this.handleChange.bind(this), this.props.onChange); @@ -69,7 +79,7 @@ class CheckBox extends React.Component { getIconProps() { return { 'aria-checked': this.getValue(), - className: 'checkbox--icon', + className: 'checkbox__icon', onKeyDown: callback(this.handleIconKeyDown.bind(this), this.props.onKeyDown), role: "checkbox", tabIndex: 0 diff --git a/client/src/core-components/checkbox.scss b/client/src/core-components/checkbox.scss index 9e09a1c5..68d3a18d 100644 --- a/client/src/core-components/checkbox.scss +++ b/client/src/core-components/checkbox.scss @@ -5,11 +5,11 @@ border-radius: 5px; display: inline-block; - &--box { + &__box { display: none; } - &--icon { + &__icon { color: $light-grey; outline: none; @@ -18,8 +18,12 @@ } } + &__label { + margin-left: 10px; + } + &_checked { - .checkbox--icon { + .checkbox__icon { color: $primary-red; &:focus { diff --git a/client/src/core-components/drop-down.scss b/client/src/core-components/drop-down.scss index facd5c53..7082e708 100644 --- a/client/src/core-components/drop-down.scss +++ b/client/src/core-components/drop-down.scss @@ -49,4 +49,17 @@ border: 1px solid $light-grey; } } + + &_large { + width: 300px; + + .drop-down__current-item { + border-radius: 4px; + } + + .drop-down__list-container { + width: 300px; + border: 1px solid $light-grey; + } + } } \ No newline at end of file diff --git a/client/src/core-components/form-field.js b/client/src/core-components/form-field.js index ad93729a..78cb9dad 100644 --- a/client/src/core-components/form-field.js +++ b/client/src/core-components/form-field.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import Input from 'core-components/input'; import DropDown from 'core-components/drop-down'; import Checkbox from 'core-components/checkbox'; +import CheckboxGroup from 'core-components/checkbox-group'; import TextEditor from 'core-components/text-editor'; class FormField extends React.Component { @@ -21,7 +22,7 @@ class FormField extends React.Component { required: React.PropTypes.bool, error: React.PropTypes.string, value: React.PropTypes.any, - field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox']), + field: React.PropTypes.oneOf(['input', 'textarea', 'select', 'checkbox', 'checkbox-group']), fieldProps: React.PropTypes.object }; @@ -36,6 +37,9 @@ class FormField extends React.Component { else if (field === 'checkbox') { return false; } + else if (field === 'checkbox-group') { + return []; + } else if (field === 'textarea') { return RichTextEditor.createEmptyValue(); } @@ -45,7 +49,7 @@ class FormField extends React.Component { } render() { - const Wrapper = (this.props.field === 'textarea') ? 'div' : 'label'; + const Wrapper = (_.includes(this.getDivTypes(), this.props.field)) ? 'div' : 'label'; const fieldContent = [ {this.props.label}, this.renderField(), @@ -67,7 +71,8 @@ class FormField extends React.Component { 'input': Input, 'textarea': TextEditor, 'select': DropDown, - 'checkbox': Checkbox + 'checkbox': Checkbox, + 'checkbox-group': CheckboxGroup }[this.props.field]; if(this.props.decorator) { @@ -122,6 +127,13 @@ class FormField extends React.Component { return props; } + getDivTypes() { + return [ + 'textarea', + 'checkbox-group' + ]; + } + onChange(nativeEvent) { let event = nativeEvent; diff --git a/client/src/data/fixtures/staff-fixtures.js b/client/src/data/fixtures/staff-fixtures.js index 4aadb436..b0180b7b 100644 --- a/client/src/data/fixtures/staff-fixtures.js +++ b/client/src/data/fixtures/staff-fixtures.js @@ -536,6 +536,7 @@ module.exports = [ pages: 4 } } + } }, { @@ -577,5 +578,68 @@ module.exports = [ } } } + }, + { + path: '/staff/get-all', + time: 100, + response: function() { + return { + status: 'success', + data: [ + { + profilePic: 'http://www.opensupports.com/profilepic.jpg', + name: 'Emilia Clarke', + departments: [{id: 2, name: 'Technical issues'}], + assignedTickets: 4, + closedTickets: 21, + lastLogin: 20161212 + }, + { + profilePic: 'http://www.opensupports.com/profilepic.jpg', + name: 'Yulian A GUI Yermo', + departments: [{id: 2, name: 'Technical issues'}, {id: 1, name: 'Sales Support'}], + assignedTickets: 9, + closedTickets: 0, + lastLogin: 20161212 + }, + { + profilePic: 'http://www.opensupports.com/profilepic.jpg', + name: 'Miltona Costa', + departments: [{id: 1, name: 'Sales Support'}], + assignedTickets: -1, + closedTickets: -1, + lastLogin: 20160212 + }, + { + profilePic: 'http://www.opensupports.com/profilepic.jpg', + name: 'Emiliasnikova Rusachestkvuy', + departments: [{id: 1, name: 'Sales Support'}, {id: 3, name: 'System and Administration'}], + assignedTickets: 100, + closedTickets: 21, + lastLogin: 20130101 + }, + { + profilePic: 'http://www.opensupports.com/profilepic.jpg', + name: 'Laurita Morrechaga Rusachestkvuy', + departments: [{id: 3, name: 'System and Administration'}], + assignedTickets: 1, + closedTickets: 1, + lastLogin: 2012050 + } + ] + }; + } + }, + { + path: '/staff/add', + time: 100, + response: function () { + return { + status: 'success', + data: { + staffId: 5 + } + }; + } } ]; \ No newline at end of file diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index 0f60a85e..d67c1dcb 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -96,6 +96,15 @@ export default { 'DELETE_AND_BAN': 'Delete and ban', 'STAFF_LEVEL': 'Staff Level', 'ASSIGNED': 'Assigned', + 'ASSIGNED_TICKETS': '{tickets} assigned tickets', + 'CLOSED_TICKETS': '{tickets} closed tickets', + 'LAST_LOGIN': 'Last login', + 'ADD_NEW_STAFF': 'Add new staff', + 'ADD_STAFF': 'Add staff', + 'LEVEL': 'Level', + 'LEVEL_1': 'Level 1 (Tickets)', + 'LEVEL_2': 'Level 2 (Tickets + Articles)', + 'LEVEL_3': 'Level 2 (Tickets + Articles + Staff)', //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.', @@ -118,6 +127,8 @@ export default { 'ADD_ARTICLE_DESCRIPTION': 'Here you can add an article that will be available for every user. It will be added inside the category {category}.', 'LIST_ARTICLES_DESCRIPTION': 'This is a list of articles that includes information about our services.', 'ADD_TOPIC_DESCRIPTION': 'Here you can add a topic that works as a category for articles.', + 'STAFF_MEMBERS_DESCRIPTION': 'Here you can see who are your staff members.', + 'ADD_STAFF_DESCRIPTION': 'Here you can add staff members to your teams', //ERRORS 'EMAIL_OR_PASSWORD': 'Email or password invalid', diff --git a/client/src/scss/_vars.scss b/client/src/scss/_vars.scss index 59793c65..cea6e370 100644 --- a/client/src/scss/_vars.scss +++ b/client/src/scss/_vars.scss @@ -25,4 +25,37 @@ $font-size--sm: 13px; $font-size--md: 16px; $font-size--bg: 19px; $font-size--lg: 24px; -$font-size--xl: 32px; \ No newline at end of file +$font-size--xl: 32px; + +@mixin scrollbars() { + $size: .4em; + $color: $grey; + + &::-webkit-scrollbar { + width: $size; + height: $size; + } + + &::-webkit-scrollbar-thumb { + background: transparent; + } + + &::-webkit-scrollbar-track { + backgroundr: transparent; + } + + &:hover { + &::-webkit-scrollbar { + width: $size; + height: $size; + } + + &::-webkit-scrollbar-thumb { + background: $color; + } + + &::-webkit-scrollbar-track { + background: lighten($color, 50%); + } + } +} diff --git a/server/controllers/staff.php b/server/controllers/staff.php index aafedcbd..fff73e65 100644 --- a/server/controllers/staff.php +++ b/server/controllers/staff.php @@ -6,6 +6,10 @@ 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'; +require_once 'staff/add.php'; +require_once 'staff/get-all.php'; +require_once 'staff/delete.php'; +require_once 'staff/edit.php'; $systemControllerGroup = new ControllerGroup(); $systemControllerGroup->setGroupPath('/staff'); @@ -17,5 +21,9 @@ $systemControllerGroup->addController(new GetTicketStaffController); $systemControllerGroup->addController(new GetNewTicketsStaffController); $systemControllerGroup->addController(new GetAllTicketsStaffController); $systemControllerGroup->addController(new SearchTicketStaffController); +$systemControllerGroup->addController(new AddStaffController); +$systemControllerGroup->addController(new GetAllStaffController); +$systemControllerGroup->addController(new DeleteStaffController); +$systemControllerGroup->addController(new EditStaffController); $systemControllerGroup->finalize(); \ No newline at end of file diff --git a/server/controllers/staff/add.php b/server/controllers/staff/add.php new file mode 100644 index 00000000..e3b13390 --- /dev/null +++ b/server/controllers/staff/add.php @@ -0,0 +1,87 @@ + 'staff_3', + 'requestData' => [ + 'name' => [ + 'validation' => DataValidator::length(2, 55)->alpha(), + 'error' => ERRORS::INVALID_NAME + ], + 'email' => [ + 'validation' => DataValidator::email(), + 'error' => ERRORS::INVALID_EMAIL + ], + 'password' => [ + 'validation' => DataValidator::length(5, 200), + 'error' => ERRORS::INVALID_PASSWORD + ], + 'level' => [ + 'validation' => DataValidator::between(1, 3, true), + 'error' => ERRORS::INVALID_LEVEL + ] + + ] + ]; + } + + public function handler() { + $this->storeRequestData(); + $staff = new Staff(); + + $staffRow = Staff::getDataStore($this->email,'email'); + + if($staffRow->isNull()) { + $staff->setProperties([ + 'name'=> $this->name, + 'email' => $this->email, + 'password'=> Hashing::hashPassword($this->password), + 'profilePic' => $this->profilePic, + 'level' => $this->level, + 'sharedDepartmentList'=> $this->getDepartmentList(), + ]); + + + Response::respondSuccess([ + 'id' => $staff->store() + ]); + return; + } + + Response::respondError(ERRORS::ALREADY_A_STAFF); + } + + public function storeRequestData() { + $this->name = Controller::request('name'); + $this->email = Controller::request('email'); + $this->password = Controller::request('password'); + $this->profilePic = Controller::request('profilePic'); + $this->level = Controller::request('level'); + $this->departments = Controller::request('departments'); + } + + public function getDepartmentList() { + $listDepartments = new DataStoreList(); + $departmentIds = json_decode($this->departments); + + foreach($departmentIds as $id) { + $department = Department::getDataStore($id); + $listDepartments->add($department); + } + + return $listDepartments; + } +} \ No newline at end of file diff --git a/server/controllers/staff/delete.php b/server/controllers/staff/delete.php new file mode 100644 index 00000000..d9332267 --- /dev/null +++ b/server/controllers/staff/delete.php @@ -0,0 +1,34 @@ + 'staff_3', + 'requestData' => [ + 'staffId' =>[ + 'validation' => DataValidator::dataStoreId('staff'), + 'error' => ERRORS::INVALID_STAFF + ] + ] + ]; + } + + public function handler() { + $staffId = Controller::request('staffId'); + $staff = Staff::getDataStore($staffId); + + foreach($staff->sharedTicketList as $ticket) { + $ticket->owner = null; + $ticket->true = true; + $ticket->store(); + } + + $staff->delete(); + Response::respondSuccess(); + } + +} \ No newline at end of file diff --git a/server/controllers/staff/edit.php b/server/controllers/staff/edit.php new file mode 100644 index 00000000..66500cc0 --- /dev/null +++ b/server/controllers/staff/edit.php @@ -0,0 +1,71 @@ + 'staff_1', + 'requestData' => [] + ]; + } + + public function handler() { + $this->staffId = Controller::request('staffId'); + + if(!$this->staffId) { + $this->staffRow = Controller::getLoggedUser(); + } else if(Controller::isStaffLogged(3)) { + $this->staffRow = Staff::getDataStore($this->staffId, 'id'); + + if($this->staffRow->isNull()) { + Response::respondError(ERRORS::INVALID_STAFF); + return; + } + } else { + Response::respondError(ERRORS::NO_PERMISSION); + return; + } + + $this->editInformation(); + Response::respondSuccess(); + } + + public function editInformation() { + + if(Controller::request('email')) { + $this->staffRow->email = Controller::request('email'); + } + + if(Controller::request('password')) { + $this->staffRow->password = Hashing::hashPassword(Controller::request('password')); + } + + if(Controller::request('level') && Controller::isStaffLogged(3)) { + $this->staffRow->level = Controller::request('level'); + } + + if(Controller::request('departments') && Controller::isStaffLogged(3)) { + $this->staffRow->sharedDepartmentList = $this->getDepartmentList(); + } + + $this->staffRow->store(); + } + + + public function getDepartmentList() { + $listDepartments = new DataStoreList(); + $departmentIds = json_decode(Controller::request('departments')); + + foreach($departmentIds as $id) { + $department = Department::getDataStore($id); + $listDepartments->add($department); + } + + return $listDepartments; + } +} \ No newline at end of file diff --git a/server/controllers/staff/get-all.php b/server/controllers/staff/get-all.php new file mode 100644 index 00000000..2c93165a --- /dev/null +++ b/server/controllers/staff/get-all.php @@ -0,0 +1,45 @@ + 'staff_3', + 'requestData' => [] + ]; + } + + + public function handler() { + $staffs = Staff::getAll(); + $staffArray = []; + + foreach($staffs as $staff) { + $assignedTickets = 0; + $closedTickets = 0; + + foreach ($staff->sharedTicketList as $ticket) { + if($ticket->closed) $closedTickets++; + else $assignedTickets++; + } + + $staffArray[] = [ + 'id' => $staff->id, + 'name' => $staff->name, + 'email' => $staff->email, + 'profilePic' => $staff->profilePic, + 'level' => $staff->level, + 'departments' => $staff->sharedDepartmentList->toArray(), + 'assignedTickets' => $assignedTickets, + 'closedTickets' => $closedTickets, + 'lastLogin' => $staff->lastLogin + ]; + } + + Response::respondSuccess($staffArray); + + } +} \ No newline at end of file diff --git a/server/controllers/staff/get.php b/server/controllers/staff/get.php index db922c80..1cd6a491 100644 --- a/server/controllers/staff/get.php +++ b/server/controllers/staff/get.php @@ -14,6 +14,14 @@ class GetStaffController extends Controller { public function handler() { $user = Controller::getLoggedUser(); + + $userId = Controller::request('staffId'); + $userRow = Staff::getDataStore($userId); + + if($user->level == 3 && !$userRow->isNull()) { + $user = $userRow; + } + $parsedDepartmentList = []; $departmentList = $user->sharedDepartmentList; diff --git a/server/controllers/user/login.php b/server/controllers/user/login.php index 07cb3340..020eae13 100644 --- a/server/controllers/user/login.php +++ b/server/controllers/user/login.php @@ -22,6 +22,10 @@ class LoginController extends Controller { if ($this->checkInputCredentials() || $this->checkRememberToken()) { $this->createUserSession(); $this->createSessionCookie(); + if(Controller::request('staff')) { + $this->userInstance->lastLogin = Date::getCurrentDate(); + $this->userInstance->store(); + } Response::respondSuccess($this->getUserData()); } else { diff --git a/server/data/ERRORS.php b/server/data/ERRORS.php index 91387377..5f1c70b0 100644 --- a/server/data/ERRORS.php +++ b/server/data/ERRORS.php @@ -26,4 +26,7 @@ class ERRORS { const INVALID_ORDER = 'INVALID_ORDER'; const INVALID_USER = 'INVALID_USER'; const ALREADY_BANNED = 'ALREADY_BANNED'; + const INVALID_LEVEL = 'INVALID_LEVEL'; + const ALREADY_A_STAFF = 'ALREADY_A_STAFF'; + const INVALID_STAFF = 'INVALID_STAFF'; } diff --git a/server/libs/validations/dataStoreId.php b/server/libs/validations/dataStoreId.php index a510cb55..029ee33c 100644 --- a/server/libs/validations/dataStoreId.php +++ b/server/libs/validations/dataStoreId.php @@ -22,6 +22,9 @@ class DataStoreId extends AbstractRule { case 'user': $dataStore = \User::getUser($dataStoreId); break; + case 'staff': + $dataStore = \Staff::getUser($dataStoreId); + break; case 'ticket': $dataStore = \Ticket::getTicket($dataStoreId); break; @@ -45,6 +48,7 @@ class DataStoreId extends AbstractRule { private function isDataStoreNameValid($dataStoreName) { return in_array($dataStoreName, [ 'user', + 'staff', 'ticket', 'department', 'customresponse', diff --git a/server/models/Department.php b/server/models/Department.php index a263d38e..58d8cd38 100644 --- a/server/models/Department.php +++ b/server/models/Department.php @@ -24,4 +24,10 @@ class Department extends DataStore { return $departmentsNameList; } + public function toArray() { + return [ + 'id' => $this->id, + 'name' => $this->name + ]; + } } \ No newline at end of file diff --git a/server/models/Staff.php b/server/models/Staff.php index 03e3e078..cca0bc4c 100644 --- a/server/models/Staff.php +++ b/server/models/Staff.php @@ -17,7 +17,8 @@ class Staff extends DataStore { 'profilePic', 'level', 'sharedDepartmentList', - 'sharedTicketList' + 'sharedTicketList', + 'lastLogin' ]; } @@ -30,4 +31,17 @@ class Staff extends DataStore { public static function getUser($value, $property = 'id') { return parent::getDataStore($value, $property); } + + public function toArray() { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'profilePic' => $this->profilePic, + 'level' => $this->level, + 'departments' => $this->sharedDepartmentList->toArray(), + 'tickets' => $this->sharedTicketList->toArray(), + 'lastLogin' => $this->lastLogin + ]; + } } diff --git a/tests/init.rb b/tests/init.rb index 45791f55..0af3e81a 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -25,7 +25,10 @@ require './ticket/custom-response.rb' require './ticket/change-department.rb' require './ticket/close.rb' require './ticket/re-open.rb' +require './staff/add.rb' require './staff/get.rb' +require './staff/edit.rb' +require './staff/delete.rb' require './staff/assign-ticket.rb' require './staff/un-assign-ticket.rb' require './staff/get-tickets.rb' @@ -39,5 +42,6 @@ require './user/get-user.rb' require './user/ban.rb' require './user/get-users-test.rb' require './user/delete.rb' +require './staff/get-all.rb' diff --git a/tests/staff/add.rb b/tests/staff/add.rb new file mode 100644 index 00000000..5b189cc3 --- /dev/null +++ b/tests/staff/add.rb @@ -0,0 +1,42 @@ +describe'/staff/add' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should add staff member' do + result= request('/staff/add', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'Tyrion Lannister', + email: 'tyrion@opensupports.com', + password: 'testpassword', + level: 2, + profilePic: 'http://www.opensupports.com/profilepic.jpg', + departments: '[1]' + }) + + (result['status']).should.equal('success') + + row = $database.getRow('staff', result['data']['id'], 'id') + + (row['name']).should.equal('Tyrion Lannister') + (row['email']).should.equal('tyrion@opensupports.com') + (row['profile_pic']).should.equal('http://www.opensupports.com/profilepic.jpg') + (row['level']).should.equal('2') + end + it 'should fail if staff member is alrady a staff' do + result= request('/staff/add', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'Tyrion Lannister', + email: 'tyrion@opensupports.com', + password: 'testpassword', + level: 2, + profilePic: 'http://www.opensupports.com/profilepic.jpg', + departments: '[1]' + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('ALREADY_A_STAFF') + + end +end \ No newline at end of file diff --git a/tests/staff/delete.rb b/tests/staff/delete.rb new file mode 100644 index 00000000..af55dcc5 --- /dev/null +++ b/tests/staff/delete.rb @@ -0,0 +1,29 @@ +describe'/staff/delete' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should delete staff member' do + result= request('/staff/delete', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + staffId:2 + }) + + (result['status']).should.equal('success') + + row = $database.getRow('staff', 2, 'id') + (row).should.equal(nil) + + end + it 'should fail delete if staff member is does not exist' do + result= request('/staff/delete', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + staffId:2 + }) + + (result['status']).should.equal('fail') + (result['message']).should.equal('INVALID_STAFF') + + end +end \ No newline at end of file diff --git a/tests/staff/edit.rb b/tests/staff/edit.rb new file mode 100644 index 00000000..3167359f --- /dev/null +++ b/tests/staff/edit.rb @@ -0,0 +1,56 @@ +describe'/staff/edit' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should edit another staff member' do + result= request('/staff/edit', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + email: 'LittleLannister@opensupports.com', + level: 1, + departments: '[1, 2]', + staffId: 2 + }) + + (result['status']).should.equal('success') + + row = $database.getRow('staff', 2, 'id') + + (row['email']).should.equal('LittleLannister@opensupports.com') + (row['level']).should.equal('1') + + rows = $database.getRow('department_staff', 2, 'staff_id') + + (rows['department_id']).should.equal('1') + + end + + it 'should edit staff member ' do + request('/staff/add', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + name: 'Arya Stark', + password: 'starkpassword', + email: 'arya@opensupports.com', + level: 2, + profilePic: 'http://www.opensupports.com/profilepic.jpg', + departments: '[1]' + }) + request('/user/logout') + Scripts.login('arya@opensupports.com', 'starkpassword', true) + + result = request('/staff/edit', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + email: 'newwstaff@opensupports.com', + }) + + (result['status']).should.equal('success') + + row = $database.getRow('staff', $csrf_userid, 'id') + + (row['email']).should.equal('newwstaff@opensupports.com') + (row['level']).should.equal('2') + + end +end \ No newline at end of file diff --git a/tests/staff/get-all.rb b/tests/staff/get-all.rb new file mode 100644 index 00000000..ba5544f5 --- /dev/null +++ b/tests/staff/get-all.rb @@ -0,0 +1,35 @@ +describe'/staff/get-all' do + request('/user/logout') + Scripts.login($staff[:email], $staff[:password], true) + + it 'should get all staff member' do + result= request('/staff/get-all', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token + }) + + (result['status']).should.equal('success') + + (result['data'][0]['name']).should.equal('Emilia Clarke') + (result['data'][0]['email']).should.equal('staff@opensupports.com') + (result['data'][0]['profilePic']).should.equal('http://www.opensupports.com/profilepic.jpg') + (result['data'][0]['level']).should.equal('3') + (result['data'][0]['departments'][0]['id']).should.equal('1') + (result['data'][0]['departments'][0]['name']).should.equal('Tech Support') + (result['data'][0]['departments'][1]['id']).should.equal('2') + (result['data'][0]['departments'][1]['name']).should.equal('Suggestions') + (result['data'][0]['departments'][2]['id']).should.equal('3') + (result['data'][0]['departments'][2]['name']).should.equal('Sales and Subscriptions') + (result['data'][0]['assignedTickets']).should.equal(3) + (result['data'][0]['closedTickets']).should.equal(0) + + (result['data'][1]['name']).should.equal('Arya Stark') + (result['data'][1]['email']).should.equal('newwstaff@opensupports.com') + (result['data'][1]['profilePic']).should.equal('http://www.opensupports.com/profilepic.jpg') + (result['data'][1]['level']).should.equal('2') + (result['data'][1]['departments'][0]['id']).should.equal('1') + (result['data'][1]['departments'][0]['name']).should.equal('Tech Support') + (result['data'][1]['assignedTickets']).should.equal(0) + (result['data'][1]['closedTickets']).should.equal(0) + end +end \ No newline at end of file diff --git a/tests/staff/get.rb b/tests/staff/get.rb index a0abd11b..86f1870b 100644 --- a/tests/staff/get.rb +++ b/tests/staff/get.rb @@ -11,5 +11,20 @@ describe '/staff/get/' do (result['status']).should.equal('success') (result['data']['name']).should.equal('Emilia Clarke') (result['data']['staff']).should.equal(true) + (result['data']['email']).should.equal('staff@opensupports.com') + (result['data']['level']).should.equal('3') + end + it 'should return staff member data with staff Id' do + result = request('/staff/get', { + csrf_userid: $csrf_userid, + csrf_token: $csrf_token, + staffId:2 + }) + + (result['status']).should.equal('success') + (result['data']['name']).should.equal('Tyrion Lannister') + (result['data']['staff']).should.equal(true) + (result['data']['email']).should.equal('tyrion@opensupports.com') + (result['data']['level']).should.equal('2') end end \ No newline at end of file