Commit 6ea71563 authored by henrik.prangel's avatar henrik.prangel
Browse files

JUT-87 Add visual distinction between chats that require and don't require...

JUT-87 Add visual distinction between chats that require and don't require customer service intervention
parent a3035925
......@@ -83,7 +83,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
.antMatchers("/api/conversation").permitAll()
.antMatchers("/api/conversation/user/commit").permitAll()
.antMatchers("/api/messages/vote").permitAll()
.antMatchers("/api/chats/intervention/*").permitAll()
.antMatchers("/api/chats/{id}/needs-customer-service").permitAll()
.antMatchers("/api/feedbacks").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/websocket/tracker").hasAuthority(AuthoritiesConstants.ANONYMOUS)
......
......@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -30,9 +31,12 @@ public class MessageService {
private final MessageMapper messageMapper;
public MessageService(MessageRepository messageRepository, MessageMapper messageMapper) {
private final SimpMessageSendingOperations messagingTemplate;
public MessageService(MessageRepository messageRepository, MessageMapper messageMapper, SimpMessageSendingOperations messagingTemplate) {
this.messageRepository = messageRepository;
this.messageMapper = messageMapper;
this.messagingTemplate = messagingTemplate;
}
/**
......@@ -107,4 +111,9 @@ public class MessageService {
throw new IllegalArgumentException("No such vote value:" + messageWithVote.getVote());
}
}
public void broadcastMessageToUserGroup(String usergroup, MessageDTO messageDTO) {
log.debug("Broadcasting message: {} to userGroup {}", messageDTO, usergroup);
messagingTemplate.convertAndSend("/topic/admin/" + usergroup, messageDTO);
}
}
......@@ -3,7 +3,9 @@ package com.netgroup.riigibot.web.rest;
import com.netgroup.riigibot.domain.BotNameConstants;
import com.netgroup.riigibot.security.AuthoritiesConstants;
import com.netgroup.riigibot.service.ChatService;
import com.netgroup.riigibot.service.MessageService;
import com.netgroup.riigibot.service.dto.ChatDTO;
import com.netgroup.riigibot.service.dto.MessageDTO;
import com.netgroup.riigibot.web.rest.errors.BadRequestAlertException;
import io.github.jhipster.web.util.HeaderUtil;
import io.github.jhipster.web.util.PaginationUtil;
......@@ -35,13 +37,18 @@ public class ChatResource {
private static final String ENTITY_NAME = "chat";
private static final String CHAT_REQUIRES_CUSTOMER_SERVICE = "CHAT_REQUIRES_CUSTOMER_SERVICE";
@Value("${jhipster.clientApp.name}")
private String applicationName;
private final ChatService chatService;
public ChatResource(ChatService chatService) {
private final MessageService messageService;
public ChatResource(ChatService chatService, MessageService messageService) {
this.chatService = chatService;
this.messageService = messageService;
}
/**
......@@ -149,23 +156,21 @@ public class ChatResource {
.build();
}
@PostMapping("/chats/intervention/{id}")
public ResponseEntity<Void> updateCustomerServiceRequirement(@PathVariable Long id, @RequestBody Boolean needsCustomerService) {
log.debug("REST request to update chat: {} with needsCustomerService : {}", id, needsCustomerService);
@PutMapping("/chats/{id}/needs-customer-service")
public ResponseEntity<Void> updateCustomerServiceRequirement(@PathVariable Long id) {
log.debug("REST request to update needs-customer-service for chat: {}", id);
try {
ChatDTO chat = chatService.findOne(id).orElseThrow(NoSuchElementException::new);
chat.setNeedsCustomerService(needsCustomerService);
chatService.save(chat);
HttpHeaders entityUpdateAlert = HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, id.toString());
ChatDTO chat = chatService.findOne(id).orElseThrow(NoSuchElementException::new);
chat.setNeedsCustomerService(true);
chatService.save(chat);
return ResponseEntity.noContent().headers(entityUpdateAlert).build();
} catch (NoSuchElementException ex) {
log.error("No Chat found with id: {}", id);
HttpHeaders failureAlert = HeaderUtil.createFailureAlert(applicationName, true, ENTITY_NAME, ex.getMessage(), id.toString());
MessageDTO messageDTO = new MessageDTO();
messageDTO.setChatId(chat.getId());
messageDTO.setText(CHAT_REQUIRES_CUSTOMER_SERVICE);
messageService.broadcastMessageToUserGroup(chat.getCommittedBot(), messageDTO);
return ResponseEntity.noContent().headers(failureAlert).build();
}
HttpHeaders entityUpdateAlert = HeaderUtil.createEntityUpdateAlert(applicationName, true, ENTITY_NAME, id.toString());
return ResponseEntity.noContent().headers(entityUpdateAlert).build();
}
public List<String> getListOfBotsUserHasAuthorityToAccess(Authentication authentication) {
......
......@@ -37,12 +37,8 @@ public class MessageResource {
private String applicationName;
private final MessageService messageService;
private final SimpMessageSendingOperations messagingTemplate;
public MessageResource(MessageService messageService, SimpMessageSendingOperations messagingTemplate) {
this.messageService = messageService;
this.messagingTemplate = messagingTemplate;
}
public MessageResource(MessageService messageService) { this.messageService = messageService; }
/**
* {@code POST /messages} : Create a new message.
......@@ -150,8 +146,8 @@ public class MessageResource {
@PostMapping("/messages/usergroup/{usergroup}")
public MessageDTO broadCastMessageToUserGroup(@RequestBody MessageDTO messageDTO, @PathVariable String usergroup) {
log.debug("Broadcasting message: {} to userGroup {}", messageDTO, usergroup);
messagingTemplate.convertAndSend("/topic/admin/" + usergroup, messageDTO);
log.debug("REST request to broadcast message: {} to userGroup {}", messageDTO, usergroup);
messageService.broadcastMessageToUserGroup(usergroup, messageDTO);
return messageDTO;
}
......
......@@ -10,7 +10,12 @@ import { ACTION_TYPES as CHAT_ACTIONS } from 'app/modules/customer-service/chats
import { ACTION_TYPES as AUTH_ACTIONS } from 'app/shared/reducers/authentication';
import { SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util';
import { BOT_NAMES, REMOVE_CHAT, USER_HAS_LEFT } from 'app/shared/util/customer-service.constants';
import {
BOT_NAMES,
CHAT_REQUIRES_CUSTOMER_SERVICE,
REMOVE_CHAT,
USER_HAS_LEFT
} from 'app/shared/util/customer-service.constants';
import { AUTHORITIES } from "app/config/constants";
let stompClient = null;
......@@ -152,6 +157,11 @@ export default store => next => action => {
return;
}
if (activity.text === CHAT_REQUIRES_CUSTOMER_SERVICE) {
store.dispatch({ type: CHAT_ACTIONS.UPDATE_NEEDS_CUSTOMER_SERVICE, payload: activity });
return;
}
if (activity.authorId && activity.authorId !== accountId) {
store.dispatch({ type: CHAT_ACTIONS.UPDATE_ACTIVE_CHAT, payload: activity });
}
......
......@@ -14,6 +14,7 @@ export const ACTION_TYPES = {
START_CONVERSATION: 'adminChat/START_CONVERSATION',
SET_VISIBILITY_STATE: 'adminChat/SET_VISIBILITY_STATE',
UPDATE_ACTIVE_CHAT: 'adminChat/UPDATE_ACTIVE_CHAT',
UPDATE_NEEDS_CUSTOMER_SERVICE: 'adminChat/UPDATE_NEEDS_CUSTOMER_SERVICE',
END_CHAT: 'adminChat/END_CHAT',
};
......@@ -124,6 +125,14 @@ export default (state = initialState, action) => {
draftState.chatList = state.chatList.filter(chat => chat.id !== action.payload.chatId);
});
}
case ACTION_TYPES.UPDATE_NEEDS_CUSTOMER_SERVICE: {
return produce(state, draftState => {
const {chatId} = action.payload;
const index = draftState.chatList.findIndex(chat => chatId === chat.id);
const chatToUpdate = draftState.chatList[index];
chatToUpdate.needsCustomerService = true;
});
}
default:
return state;
}
......
......@@ -11,7 +11,7 @@ import Messages from "app/modules/customer-service/messages";
export interface ICustomerServiceChatProp extends StateProps, RouteComponentProps<{ id: string }> {}
export const CustomerServiceChat = (props: ICustomerServiceChatProp) => {
const {filteredChats, userLoginName, activeChat, match} = props
const {filteredChats, userLoginName, activeChat, currentVisibilityState, match} = props
const dispatch = useDispatch();
const setNewActiveChat = (chatId) => {
......@@ -19,9 +19,18 @@ export const CustomerServiceChat = (props: ICustomerServiceChatProp) => {
dispatch(getMessagesByChatId(chatId))
}
const isChatCommittedToCurrentUser = (chat) => {
return chat.committedAdmin === userLoginName
}
const doesChatRequireCustomerService = (chat) => {
return chat.needsCustomerService
}
return (
<div className="chat-container">
<ChatList chatsToDisplay={filteredChats}
divideChatsBy={currentVisibilityState === VISIBILITY_STATES.UNANSWERED ? doesChatRequireCustomerService : isChatCommittedToCurrentUser}
activeChatId={activeChat ? activeChat.id : null}
userLoginName={userLoginName}
match={match}
......@@ -53,6 +62,7 @@ const mapStateToProps = storeState => {
return {
filteredChats,
userLoginName,
currentVisibilityState,
activeChat: storeState.customerService.chats.activeChat,
};
}
......
......@@ -5,7 +5,7 @@ import { Nav } from 'reactstrap';
import { setVisiblityState, VISIBILITY_STATES } from "app/modules/customer-service/chats/customer-service-chat.reducer";
import Tab from "app/shared/common-components/navigation/tab";
export const Tabs = ({totalUnanswered, totalMyAnswered, totalAnswered, totalEnded, firstName, match}) => {
export const Tabs = ({totalRequireHelpUnanswered, totalUnanswered, totalMyAnswered, totalAnswered, totalEnded, firstName, match}) => {
const dispatch = useDispatch();
useEffect(() => {
......@@ -25,11 +25,11 @@ export const Tabs = ({totalUnanswered, totalMyAnswered, totalAnswered, totalEnde
return (
<Nav tabs className="rooms-filter">
<Tab name={translate("customerService.tabs.unanswered") + " (" + totalUnanswered + ")"}
<Tab name={`${translate("customerService.tabs.unanswered")} (${totalRequireHelpUnanswered}/${totalUnanswered})`}
to={`${match.url}/${VISIBILITY_STATES.UNANSWERED}`}/>
<Tab name={translate("customerService.tabs.answered") + " (" + (totalMyAnswered || 0) + "/" + (totalAnswered || 0) + ")"}
<Tab name={`${translate("customerService.tabs.answered")} (${totalMyAnswered || 0}/${totalAnswered || 0})`}
to={`${match.url}/${VISIBILITY_STATES.ANSWERED}`}/>
<Tab name={translate("customerService.tabs.ended") + " (" + totalEnded + ")"}
<Tab name={`${translate("customerService.tabs.ended")} (${totalEnded})`}
to={`${match.url}/${VISIBILITY_STATES.ENDED}`}/>
<div className="username">{firstName}</div>
</Nav>
......@@ -39,6 +39,7 @@ export const Tabs = ({totalUnanswered, totalMyAnswered, totalAnswered, totalEnde
const mapStateToProps = storeState => {
const chats = storeState.customerService.chats.chatList;
const userLoginName = storeState.authentication.account.login;
const totalRequireHelpUnanswered = chats.filter(chat => !chat.committedAdmin && !chat.ended && chat.needsCustomerService).length;
const totalUnanswered = chats.filter(chat => !chat.committedAdmin && !chat.ended).length;
const totalMyAnswered = chats.filter(chat => chat.committedAdmin === userLoginName && !chat.ended).length;
const totalAnswered = chats.filter(chat => chat.committedAdmin && !chat.ended).length;
......@@ -46,6 +47,7 @@ const mapStateToProps = storeState => {
const firstName = storeState.authentication.account.firstName;
return {
totalRequireHelpUnanswered,
totalUnanswered,
totalMyAnswered,
totalAnswered,
......
......@@ -15,6 +15,13 @@
border-top: 0;
border-radius: var(--general-border-radius);
font-family: 'Roboto', sans-serif;
.dropdown-item {
background: white;
}
.dropdown-item:hover {
background: var(--global-button-color-secondary);
color: white;
}
button {
background: var(--global-button-color-primary);
}
......
......@@ -6,7 +6,7 @@ import Chat from 'app/shared/common-components/chat-list/chat';
import '../common-components.scss'
export const ChatList = props => {
const {activeChatId, userLoginName, chatsToDisplay, match, onChatClick} = props;
const {activeChatId, userLoginName, chatsToDisplay, match, onChatClick, divideChatsBy} = props;
const history = useHistory();
if (!chatsToDisplay || chatsToDisplay && !chatsToDisplay.length) {
......@@ -19,9 +19,9 @@ export const ChatList = props => {
);
}
const moveResponderToTop = (responder, array) => {
const responderChats = array.filter(e => e.committedAdmin === responder);
const other = array.filter(e => e.committedAdmin !== responder);
const divideChatsByFilter = (array) => {
const responderChats = array.filter(chat => divideChatsBy(chat));
const other = array.filter(chat => !divideChatsBy(chat));
if (other.length && responderChats.length) {
other.unshift({divider: true});
}
......@@ -33,10 +33,18 @@ export const ChatList = props => {
onChatClick(chatId)
}
const getChatsToDisplay = () => {
if (divideChatsBy instanceof Function) {
return divideChatsByFilter(chatsToDisplay.slice().sort(sortByDateUpdated));
} else {
return chatsToDisplay.slice().sort(sortByDateUpdated);
}
}
return (
<div className="room">
<div className="chat-list">
{moveResponderToTop(userLoginName, chatsToDisplay.slice().sort(sortByDateUpdated)).map(
{getChatsToDisplay().map(
({id, divider, lastMessageText, updated, committedAdmin}) => {
if (divider) {
return <hr key="divider"/>;
......@@ -44,7 +52,6 @@ export const ChatList = props => {
return (
<Chat
key={id}
match={match}
onClickHandler={() => onChatClickHandler(id)}
active={parseInt(activeChatId, 10) === id}
roomId={id}
......
export const USER_HAS_LEFT = 'USER_HAS_LEFT';
export const CHAT_INSTITUTION_CHANGED = 'CHAT_INSTITUTION_CHANGED';
export const CHAT_REQUIRES_CUSTOMER_SERVICE = 'CHAT_REQUIRES_CUSTOMER_SERVICE';
export const REMOVE_CHAT = 'REMOVE_CHAT';
export const BOT_NAMES = {
PPA: 'bot_ppa',
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment