diff --git a/client/src/app-components/activity-row.js b/client/src/app-components/activity-row.js new file mode 100644 index 00000000..dab5d581 --- /dev/null +++ b/client/src/app-components/activity-row.js @@ -0,0 +1,129 @@ +import React from 'react'; +import _ from 'lodash'; +import {Link} from 'react-router'; + +import Icon from 'core-components/icon'; + +import i18n from 'lib-app/i18n'; + +class ActivityRow extends React.Component { + + static propTypes = { + mode: React.PropTypes.oneOf(['staff', 'system']), + type: React.PropTypes.oneOf([ + 'COMMENT', + 'ASSIGN', + 'UN_ASSIGN', + 'CLOSE', + 'CREATE_TICKET', + 'RE_OPEN', + 'DEPARTMENT_CHANGED', + 'PRIORITY_CHANGED', + + 'EDIT_SETTINGS', + 'SIGNUP', + 'ADD_TOPIC', + 'ADD_ARTICLE', + 'DELETE_TOPIC', + 'DELETE_ARTICLE', + 'EDIT_ARTICLE', + 'ADD_STAFF', + 'ADD_DEPARTMENT', + 'DELETE_DEPARTMENT', + 'EDIT_DEPARTMENT', + 'ADD_CUSTOM_RESPONSE', + 'DELETE_CUSTOM_RESPONSE', + 'EDIT_CUSTOM_RESPONSE', + 'BAN_USER', + 'DELETE_USER', + 'UN_BAN_USER' + ]), + to: React.PropTypes.string, + ticketNumber: React.PropTypes.string, + author: React.PropTypes.shape({ + name: React.PropTypes.string, + staff: React.PropTypes.bool, + id: React.PropTypes.string + }) + }; + + render() { + let ticketRelatedTypes = [ + 'COMMENT', + 'ASSIGN', + 'UN_ASSIGN', + 'CLOSE', + 'CREATE_TICKET', + 'RE_OPEN', + 'DEPARTMENT_CHANGED', + 'PRIORITY_CHANGED' + ]; + + return ( +
+ + + + {this.props.author.name} + + + {i18n('ACTIVITY_' + this.props.type)} + {_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : null} + +
+ ); + } + + renderTicketNumber() { + let ticketNumber = (this.props.mode === 'staff') ? this.props.ticketNumber : this.props.to; + + return ( + + + #{ticketNumber} + + + ); + } + + getNameLinkDestination() { + return (this.props.author.staff ? '/admin/panel/staff/view-staff/' : '/admin/panel/users/view-user/') + this.props.author.id; + } + + getIconProps() { + const iconName = { + 'COMMENT': 'comment-o', + 'ASSIGN': 'user', + 'UN_ASSIGN': 'user-times', + 'CLOSE': 'lock', + 'CREATE_TICKET': 'ticket', + 'RE_OPEN': 'unlock-alt', + 'DEPARTMENT_CHANGED': 'exchange', + 'PRIORITY_CHANGED': 'exclamation', + + 'EDIT_SETTINGS': 'wrench', + 'SIGNUP': 'user-plus', + 'ADD_TOPIC': 'book', + 'ADD_ARTICLE': 'book', + 'DELETE_TOPIC': 'book', + 'DELETE_ARTICLE': 'book', + 'EDIT_ARTICLE': 'book', + 'ADD_STAFF': 'id-card', + 'ADD_DEPARTMENT': 'university', + 'DELETE_DEPARTMENT': 'university', + 'EDIT_DEPARTMENT': 'university', + 'ADD_CUSTOM_RESPONSE': 'file', + 'DELETE_CUSTOM_RESPONSE': 'file', + 'EDIT_CUSTOM_RESPONSE': 'file', + 'BAN_USER': 'user-times', + 'DELETE_USER': 'user-times', + 'UN_BAN_USER': 'user' + }; + + return { + name: iconName[this.props.type] + } + } +} + +export default ActivityRow; \ No newline at end of file diff --git a/client/src/app-components/activity-row.scss b/client/src/app-components/activity-row.scss new file mode 100644 index 00000000..866d2aca --- /dev/null +++ b/client/src/app-components/activity-row.scss @@ -0,0 +1,26 @@ +@import "../scss/vars"; + +.activity-row { + text-align: left; + font-size: $font-size--sm; + + &__icon { + margin: 0 10px; + } + + &__name-link { + + } + + &__message { + + } + + &__ticket-link { + + } +} + +.separator { + margin: 15px; +} \ No newline at end of file diff --git a/client/src/app/admin/panel/dashboard/activity-list.js b/client/src/app/admin/panel/dashboard/activity-list.js new file mode 100644 index 00000000..9a125793 --- /dev/null +++ b/client/src/app/admin/panel/dashboard/activity-list.js @@ -0,0 +1,90 @@ +import React from 'react'; + +import API from 'lib-app/api-call'; +import i18n from 'lib-app/i18n'; + +import ActivityRow from 'app-components/activity-row'; +import SubmitButton from 'core-components/submit-button'; + +class ActivityList extends React.Component { + + static propTypes = { + mode: React.PropTypes.oneOf(['staff', 'system']) + }; + + static childContextTypes = { + loading: React.PropTypes.bool + }; + + getChildContext() { + return { + loading: this.state.loading + }; + } + + state = { + activities: [], + page: 1, + limit: false, + loading: false + }; + + componentDidMount() { + this.retrieveNextPage(); + } + + render() { + if (this.props.mode === 'staff') { + return ( +
+ {this.state.activities.map(this.renderRow.bind(this))} + {(!this.state.limit) ? this.renderButton() : null} +
+ ); + } + else { + return ( +
+ {this.state.activities.map(this.renderRow.bind(this))} + {(!this.state.limit) ? this.renderButton() : null} +
+ ); + } + } + + renderButton() { + return ( + + {i18n('LOAD_MORE')} + + ); + } + + renderRow(row) { + return ( + + ); + } + + retrieveNextPage() { + this.setState({loading: true}); + + API.call({ + path: '/staff/last-events', + data: { + page: this.state.page + } + }).then(this.onRetrieveSuccess.bind(this)); + } + + onRetrieveSuccess(result) { + this.setState({ + activities: this.state.activities.concat(result.data), + page: this.state.page + 1, + limit: (result.data.length !== 10), + loading: false + }); + } +} + +export default ActivityList; \ No newline at end of file diff --git a/client/src/app/admin/panel/dashboard/activity-list.scss b/client/src/app/admin/panel/dashboard/activity-list.scss new file mode 100644 index 00000000..e69de29b diff --git a/client/src/app/admin/panel/dashboard/admin-panel-activity.js b/client/src/app/admin/panel/dashboard/admin-panel-activity.js index 9b0cc102..4b6539e6 100644 --- a/client/src/app/admin/panel/dashboard/admin-panel-activity.js +++ b/client/src/app/admin/panel/dashboard/admin-panel-activity.js @@ -1,14 +1,116 @@ import React from 'react'; +import API from 'lib-app/api-call'; +import i18n from 'lib-app/i18n'; + +import ActivityRow from 'app-components/activity-row'; +import Header from 'core-components/header'; +import Menu from 'core-components/menu'; +import SubmitButton from 'core-components/submit-button'; + class AdminPanelActivity extends React.Component { + static childContextTypes = { + loading: React.PropTypes.bool + }; + + getChildContext() { + return { + loading: this.state.loading + }; + } + + state = { + activities: [], + page: 1, + limit: false, + loading: false, + mode: 'staff' + }; + + componentDidMount() { + this.retrieveNextPage(); + } + render() { return ( -
- /admin/panel/activity +
+
+ + {this.renderList()}
); } + + getMenuProps() { + return { + className: 'admin-panel-activity__menu', + type: 'horizontal-list-bright', + onItemClick: this.onMenuItemClick.bind(this), + tabbable: true, + items: [ + { + content: i18n('MY_NOTIFICATIONS'), + icon: '' + }, + { + content: i18n('ALL_NOTIFICATIONS'), + icon: '' + } + ] + }; + } + + renderList() { + return ( +
+ {this.state.activities.map(this.renderRow.bind(this))} + {(!this.state.limit) ? this.renderButton() : null} +
+ ); + } + + renderButton() { + return ( + + {i18n('LOAD_MORE')} + + ); + } + + renderRow(row, index) { + return ( + + ); + } + + onMenuItemClick(index) { + this.setState({ + page: 0, + mode: (index === 0) ? 'staff' : 'system', + activities: [] + }, this.retrieveNextPage.bind(this)); + } + + retrieveNextPage() { + this.setState({loading: true}); + + API.call({ + path: (this.state.mode === 'staff') ? '/staff/last-events' : '/system/get-logs', + data: { + page: this.state.page + } + }).then(this.onRetrieveSuccess.bind(this)); + } + + onRetrieveSuccess(result) { + this.setState({ + activities: this.state.activities.concat(result.data), + page: this.state.page + 1, + limit: (result.data.length !== 10), + loading: false + }); + } } export default AdminPanelActivity; \ No newline at end of file diff --git a/client/src/app/admin/panel/dashboard/admin-panel-activity.scss b/client/src/app/admin/panel/dashboard/admin-panel-activity.scss new file mode 100644 index 00000000..f3f4b462 --- /dev/null +++ b/client/src/app/admin/panel/dashboard/admin-panel-activity.scss @@ -0,0 +1,7 @@ +.admin-panel-activity { + + &__menu { + margin: 0 auto 20px auto; + width: 300px; + } +} \ No newline at end of file diff --git a/client/src/core-components/menu.js b/client/src/core-components/menu.js index de7cd63e..25cd805d 100644 --- a/client/src/core-components/menu.js +++ b/client/src/core-components/menu.js @@ -11,7 +11,7 @@ class Menu extends React.Component { id: React.PropTypes.string, itemsRole: React.PropTypes.string, header: React.PropTypes.string, - type: React.PropTypes.oneOf(['primary', 'secondary', 'navigation', 'horizontal', 'horizontal-list']), + type: React.PropTypes.oneOf(['primary', 'secondary', 'navigation', 'horizontal', 'horizontal-list', 'horizontal-list-bright']), items: React.PropTypes.arrayOf(React.PropTypes.shape({ content: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number, React.PropTypes.node]), icon: React.PropTypes.string @@ -86,7 +86,8 @@ class Menu extends React.Component { 'menu_secondary': (this.props.type === 'secondary'), 'menu_navigation': (this.props.type === 'navigation'), 'menu_horizontal': (this.props.type === 'horizontal'), - 'menu_horizontal-list': (this.props.type === 'horizontal-list') + 'menu_horizontal-list': (this.props.type === 'horizontal-list'), + 'menu_horizontal-list-bright': (this.props.type === 'horizontal-list-bright') }; classes[this.props.className] = true; diff --git a/client/src/core-components/menu.scss b/client/src/core-components/menu.scss index 6fc7486a..64eab16c 100644 --- a/client/src/core-components/menu.scss +++ b/client/src/core-components/menu.scss @@ -132,7 +132,8 @@ $transition: background-color 0.3s ease, color 0.3s ease; } } - &_horizontal-list { + &_horizontal-list, + &_horizontal-list-bright { text-align: left; background-color: $secondary-blue; min-height: 45px; @@ -165,6 +166,32 @@ $transition: background-color 0.3s ease, color 0.3s ease; outline: none; } + .menu__list-item_selected, + .menu__list-item_selected:hover { + transition: $transition; + padding: 2px 13px; + border-radius: 30px; + background-color: $primary-blue; + color: white; + } + } + + &_horizontal-list-bright { + background-color: transparent; + + .menu__list-item { + transition: none; + background-color: transparent; + color: black; + cursor: pointer; + display: inline-block; + margin-top: 11px; + margin-bottom: 10px; + margin-left: 20px; + padding: 2px 13px; + } + + .menu__list-item_selected, .menu__list-item_selected:hover { transition: $transition; diff --git a/client/src/data/fixtures/staff-fixtures.js b/client/src/data/fixtures/staff-fixtures.js index 0de75e1f..ac7a07ee 100644 --- a/client/src/data/fixtures/staff-fixtures.js +++ b/client/src/data/fixtures/staff-fixtures.js @@ -1057,5 +1057,114 @@ module.exports = [ data: {} }; } + }, + { + path: '/staff/last-events', + time: 300, + response: function(data) { + + if(data.page > 5) { + return { + status: 'success', + data: [] + }; + } + + return { + status: 'success', + data: [ + { + "type": "COMMENT", + "ticketNumber": "683061", + "author": { + "name": "Tyrion Lannister", + "staff": false, + "id": "10" + } + }, + { + "type": "RE_OPEN", + "ticketNumber": "683061", + "author": { + "name": "Tyrion Lannister", + "staff": false, + "id": "10" + } + }, + { + "type": "CLOSE", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "DEPARTMENT_CHANGED", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "PRIORITY_CHANGED", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "ASSIGN", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "UN_ASSIGN", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "COMMENT", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "ASSIGN", + "ticketNumber": "683061", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + }, + { + "type": "PRIORITY_CHANGED", + "ticketNumber": "608120", + "author": { + "name": "Emilia Clarke", + "staff": true, + "id": "1" + } + } + ] + }; + } } ]; diff --git a/client/src/data/fixtures/system-fixtures.js b/client/src/data/fixtures/system-fixtures.js index 5d410a10..a9f5687c 100644 --- a/client/src/data/fixtures/system-fixtures.js +++ b/client/src/data/fixtures/system-fixtures.js @@ -156,5 +156,106 @@ module.exports = [ ] }; } + }, + { + path: '/system/get-logs', + time: 300, + response: function() { + return { + "status": "success", + "data": [ + { + "type": "EDIT_SETTINGS", + "to": null, + "author": { + "name": "Emilia Clarke", + "id": "1", + "staff": true + } + }, + { + "type": "SIGNUP", + "to": null, + "author": { + "name": "Steve Jobs", + "id": "1", + "staff": false + } + }, + { + "type": "SIGNUP", + "to": null, + "author": { + "name": "steve jobs", + "id": "2", + "staff": false + } + }, + { + "type": "SIGNUP", + "to": null, + "author": { + "name": "steve jobs", + "id": "3", + "staff": false + } + }, + { + "type": "SIGNUP", + "to": null, + "author": { + "name": "Creator", + "id": "5", + "staff": false + } + }, + { + "type": "CREATE_TICKET", + "to": "739228", + "author": { + "name": "Creator", + "id": "5", + "staff": false + } + }, + { + "type": "CREATE_TICKET", + "to": "915839", + "author": { + "name": "Creator", + "id": "5", + "staff": false + } + }, + { + "type": "CREATE_TICKET", + "to": "192450", + "author": { + "name": "Creator", + "id": "5", + "staff": false + } + }, + { + "type": "CREATE_TICKET", + "to": "369061", + "author": { + "name": "Creator", + "id": "5", + "staff": false + } + }, + { + "type": "SIGNUP", + "to": null, + "author": { + "name": "Commenter", + "id": "6", + "staff": false + } + } + ] + }; + } } ]; diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index d3df05aa..3f193dd2 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -141,6 +141,37 @@ export default { 'OFF': 'Off', 'BOXED': 'Boxed', 'FULL_WIDTH': 'Full width', + 'LOAD_MORE': 'Load More', + 'MY_NOTIFICATIONS': 'My notifications', + 'ALL_NOTIFICATIONS': 'All notifications', + + //ACTIVITIES + 'ACTIVITY_COMMENT': 'commented ticket', + 'ACTIVITY_ASSIGN': 'assigned ticket', + 'ACTIVITY_UN_ASSIGN': 'unassigned ticket', + 'ACTIVITY_CLOSE': 'closed ticket', + 'ACTIVITY_CREATE_TICKET': 'created ticket', + 'ACTIVITY_RE_OPEN': 'reopened ticket', + 'ACTIVITY_DEPARTMENT_CHANGED': 'changed department of ticket', + 'ACTIVITY_PRIORITY_CHANGED': 'changed priority of ticket', + + 'ACTIVITY_EDIT_SETTINGS': 'edited settings', + 'ACTIVITY_SIGNUP': 'signed up', + 'ACTIVITY_ADD_TOPIC': 'added topic', + 'ACTIVITY_ADD_ARTICLE': 'added article', + 'ACTIVITY_DELETE_TOPIC': 'deleted topic', + 'ACTIVITY_DELETE_ARTICLE': 'deleted article', + 'ACTIVITY_EDIT_ARTICLE': 'edited article', + 'ACTIVITY_ADD_STAFF': 'added staff', + 'ACTIVITY_ADD_DEPARTMENT': 'added department', + 'ACTIVITY_DELETE_DEPARTMENT': 'deleted department', + 'ACTIVITY_EDIT_DEPARTMENT': 'edited department', + 'ACTIVITY_ADD_CUSTOM_RESPONSE': 'added custom response', + 'ACTIVITY_DELETE_CUSTOM_RESPONSE': 'deleted custom response', + 'ACTIVITY_EDIT_CUSTOM_RESPONSE': 'edited custom response', + 'ACTIVITY_BAN_USER': 'banned user', + 'ACTIVITY_DELETE_USER': 'deleted user', + 'ACTIVITY_UN_BAN_USER': 'banned user', //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.', diff --git a/server/controllers/system/get-logs.php b/server/controllers/system/get-logs.php index 52f0224b..90ffd6b9 100644 --- a/server/controllers/system/get-logs.php +++ b/server/controllers/system/get-logs.php @@ -17,7 +17,7 @@ class GetLogsController extends Controller { } public function handler() { - $page =Controller::request('page'); + $page = Controller::request('page'); $logList = Log::find('LIMIT ? OFFSET ?', [10, 10*($page-1)+1]); Response::respondSuccess($logList->toArray()); diff --git a/server/controllers/ticket/change-department.php b/server/controllers/ticket/change-department.php index 2cfe0f25..ac2e78da 100644 --- a/server/controllers/ticket/change-department.php +++ b/server/controllers/ticket/change-department.php @@ -44,7 +44,7 @@ class ChangeDepartmentController extends Controller { $ticket->unread = true; $ticket->store(); - Log::createLog('CHANGE_DEPARTMENT', $department); + Log::createLog('DEPARTMENT_CHANGED', $ticket->ticketNumber); Response::respondSuccess(); } diff --git a/server/controllers/ticket/change-priority.php b/server/controllers/ticket/change-priority.php index 989fa5ec..2e671a57 100644 --- a/server/controllers/ticket/change-priority.php +++ b/server/controllers/ticket/change-priority.php @@ -38,7 +38,7 @@ class ChangePriorityController extends Controller { $ticket->addEvent($event); $ticket->store(); - Log::createLog('CHANGE_PRIORITY', $priority); + Log::createLog('PRIORITY_CHANGED', $ticket->ticketNumber); Response::respondSuccess(); } else { Response::respondError(ERRORS::NO_PERMISSION); diff --git a/server/controllers/ticket/close.php b/server/controllers/ticket/close.php index 3080c7b2..ed983a35 100644 --- a/server/controllers/ticket/close.php +++ b/server/controllers/ticket/close.php @@ -33,7 +33,7 @@ class CloseController extends Controller { $this->ticket->store(); - Log::createLog('CLOSE_TICKET', $this->ticket); + Log::createLog('CLOSE', $this->ticket->ticketNumber); Response::respondSuccess(); } diff --git a/server/controllers/ticket/comment.php b/server/controllers/ticket/comment.php index b998ee5e..b51414c2 100644 --- a/server/controllers/ticket/comment.php +++ b/server/controllers/ticket/comment.php @@ -31,7 +31,7 @@ class CommentController extends Controller { if ($session->isLoggedWithId($this->ticket->author->id) || Controller::isStaffLogged()) { $this->storeComment(); - Log::createLog('COMMENT_TICKET', $this->ticket); + Log::createLog('COMMENT', $this->ticket->ticketNumber); Response::respondSuccess(); } else { diff --git a/server/controllers/ticket/create.php b/server/controllers/ticket/create.php index 4818ca11..ac9f4c53 100644 --- a/server/controllers/ticket/create.php +++ b/server/controllers/ticket/create.php @@ -43,7 +43,7 @@ class CreateController extends Controller { $this->storeTicket(); - Log::createLog('CREATE_TICKET', $this->title); + Log::createLog('CREATE_TICKET', $this->ticketNumber); Response::respondSuccess([ 'ticketNumber' => $this->ticketNumber ]); diff --git a/server/controllers/ticket/re-open.php b/server/controllers/ticket/re-open.php index b5f97713..b2754256 100644 --- a/server/controllers/ticket/re-open.php +++ b/server/controllers/ticket/re-open.php @@ -32,7 +32,7 @@ class ReOpenController extends Controller { $this->ticket->store(); - Log::createLog('RE_OPEN_TICKET', $this->ticket); + Log::createLog('RE_OPEN', $this->ticket->ticketNumber); Response::respondSuccess(); } diff --git a/server/controllers/user/un-ban.php b/server/controllers/user/un-ban.php index 30a83e02..d1d53d52 100644 --- a/server/controllers/user/un-ban.php +++ b/server/controllers/user/un-ban.php @@ -29,7 +29,6 @@ class UnBanUserController extends Controller { Response::respondSuccess(); } - } } \ No newline at end of file diff --git a/server/models/Log.php b/server/models/Log.php index 30038d3c..ebe24469 100644 --- a/server/models/Log.php +++ b/server/models/Log.php @@ -17,7 +17,8 @@ class Log extends DataStore { if($author === null) { $author = Controller::getLoggedUser(); } - $log = new Log(); + + $log = new Log(); $log->setProperties(array( 'type' => $type, @@ -34,10 +35,16 @@ class Log extends DataStore { } public function toArray() { + $author = ($this->authorUser instanceof User) ? $this->authorUser : $this->authorStaff; + return [ 'type' => $this->type, 'to' => $this->to, - 'author' => ($this->authorUser instanceof User) ? $this->authorUser->toArray() : $this->authorStaff->toArray() + 'author' => [ + 'name' => $author->name, + 'id' => $author->id, + 'staff' => $author instanceof Staff + ] ]; } } \ No newline at end of file diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php index be1229bf..4bd7ebb1 100644 --- a/server/models/Ticketevent.php +++ b/server/models/Ticketevent.php @@ -67,6 +67,7 @@ class Ticketevent extends DataStore { 'ticketNumber' => $this->ticket->ticketNumber, 'author' => [ 'name' => $user->name, + 'staff' => $user instanceOf Staff, 'id' => $user->id ] ];