FEAT: Editado Home page para se adequar ao serviço de pausa
This commit is contained in:
parent
fe40e8bd76
commit
4b0a4bb3e3
@ -318,14 +318,16 @@ export function ChatWindow({
|
|||||||
assignmentLabel,
|
assignmentLabel,
|
||||||
transferNote,
|
transferNote,
|
||||||
isReplying,
|
isReplying,
|
||||||
|
isPaused = false,
|
||||||
|
pauseDurationLabel = '00:00',
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
}) {
|
}) {
|
||||||
const messagesRef = useRef(null);
|
const messagesRef = useRef(null);
|
||||||
const safeContact = contact || {
|
const safeContact = contact || {
|
||||||
id: '',
|
id: '',
|
||||||
name: 'Nenhuma conversa ativa',
|
name: isPaused ? 'Atendimento pausado' : 'Nenhuma conversa ativa',
|
||||||
status: 'offline',
|
status: 'offline',
|
||||||
lastSeen: 'Aguardando fila do Omnino',
|
lastSeen: isPaused ? `Pausa em andamento: ${pauseDurationLabel}` : 'Aguardando fila do Omnino',
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -340,6 +342,62 @@ export function ChatWindow({
|
|||||||
});
|
});
|
||||||
}, [messages, isReplying]);
|
}, [messages, isReplying]);
|
||||||
|
|
||||||
|
if (isPaused) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: '#fff',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
borderRadius: '28px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||||
|
height: isMobile ? 'auto' : 'min(760px, calc(100vh - 190px))',
|
||||||
|
minHeight: isMobile ? 420 : 0,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
style={{
|
||||||
|
padding: '1.25rem 1.5rem',
|
||||||
|
borderBottom: '1px solid var(--color-border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong style={{ display: 'block', fontSize: '1.15rem' }}>Atendimento pausado</strong>
|
||||||
|
<span style={{ color: 'var(--color-text-soft)' }}>Pausa em andamento: {pauseDurationLabel}</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '1.5rem',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
minHeight: 0,
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at top left, rgba(0, 164, 183, 0.06), transparent 22%), linear-gradient(180deg, rgba(245, 248, 251, 0.8), rgba(255, 255, 255, 0.95))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 460,
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: '1.2rem',
|
||||||
|
background: '#fff',
|
||||||
|
color: 'var(--color-text-soft)',
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Voce esta em pausa ha {pauseDurationLabel}. Retome o atendimento pela Home para visualizar a fila,
|
||||||
|
assumir chamados e responder clientes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
@ -583,7 +641,9 @@ export function ChatWindow({
|
|||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Nenhuma mensagem carregada.
|
{isPaused
|
||||||
|
? `Voce esta em pausa ha ${pauseDurationLabel}. Volte da pausa para visualizar a fila e seus atendimentos.`
|
||||||
|
: 'Nenhuma mensagem carregada.'}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -625,7 +685,9 @@ export function ChatWindow({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ display: 'block' }}>
|
<span style={{ display: 'block' }}>
|
||||||
{canAssumeChat
|
{isPaused
|
||||||
|
? `Voce esta em pausa ha ${pauseDurationLabel}. Nenhum atendimento sera exibido ate voce voltar.`
|
||||||
|
: canAssumeChat
|
||||||
? 'Este atendimento está na fila. Assuma para responder ou transferir.'
|
? 'Este atendimento está na fila. Assuma para responder ou transferir.'
|
||||||
: assignmentLabel || 'Este atendimento está atribuído a outro usuário.'}
|
: assignmentLabel || 'Este atendimento está atribuído a outro usuário.'}
|
||||||
</span>
|
</span>
|
||||||
@ -681,7 +743,9 @@ export function ChatWindow({
|
|||||||
}}
|
}}
|
||||||
disabled={!safeContact.id || !canReply}
|
disabled={!safeContact.id || !canReply}
|
||||||
placeholder={
|
placeholder={
|
||||||
!safeContact.id
|
isPaused
|
||||||
|
? 'Voce esta em pausa'
|
||||||
|
: !safeContact.id
|
||||||
? 'Aguardando conversa entrar em uma fila'
|
? 'Aguardando conversa entrar em uma fila'
|
||||||
: canReply
|
: canReply
|
||||||
? 'Escreva sua mensagem...'
|
? 'Escreva sua mensagem...'
|
||||||
|
|||||||
@ -4,9 +4,33 @@ import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
|||||||
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
import { getAccessOptions, getAccessUsers } from '../../management/services/adminAccessService';
|
||||||
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
|
import { getCurrentUser, getCurrentUserProfile } from '../../auth/services/sessionService';
|
||||||
import { transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
import { transferAreas as fallbackTransferAreas } from '../services/chatMocks';
|
||||||
|
import {
|
||||||
|
getAgentPresence,
|
||||||
|
listAgentPresence,
|
||||||
|
pauseAgent,
|
||||||
|
resumeAgent,
|
||||||
|
} from '../services/agentPresenceService';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
|
const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
|
||||||
|
|
||||||
|
function getPresenceByUserId(presenceList, userId) {
|
||||||
|
return presenceList.find((presence) => Number(presence.user_id) === Number(userId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPauseDuration(totalSeconds) {
|
||||||
|
const seconds = Math.max(0, Number(totalSeconds || 0));
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${String(remainingMinutes).padStart(2, '0')}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${String(remainingMinutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function getLastMessageFromMe(messages = []) {
|
function getLastMessageFromMe(messages = []) {
|
||||||
const lastMessage = [...messages].reverse().find(isDisplayableMessage);
|
const lastMessage = [...messages].reverse().find(isDisplayableMessage);
|
||||||
if (!lastMessage) return false;
|
if (!lastMessage) return false;
|
||||||
@ -210,6 +234,10 @@ export function useChat() {
|
|||||||
const [attachedFile, setAttachedFile] = useState(null);
|
const [attachedFile, setAttachedFile] = useState(null);
|
||||||
const [areaOptions, setAreaOptions] = useState([]);
|
const [areaOptions, setAreaOptions] = useState([]);
|
||||||
const [accessUsers, setAccessUsers] = useState([]);
|
const [accessUsers, setAccessUsers] = useState([]);
|
||||||
|
const [presenceList, setPresenceList] = useState([]);
|
||||||
|
const [agentPresence, setAgentPresence] = useState(null);
|
||||||
|
const [pauseSeconds, setPauseSeconds] = useState(0);
|
||||||
|
const [isPresenceLoading, setIsPresenceLoading] = useState(false);
|
||||||
const [selectedArea, setSelectedArea] = useState('Sem fila');
|
const [selectedArea, setSelectedArea] = useState('Sem fila');
|
||||||
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
const [isTransferOpen, setIsTransferOpen] = useState(false);
|
||||||
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
|
const [transferArea, setTransferArea] = useState(currentUserAreas[0] || 'Suporte');
|
||||||
@ -221,6 +249,7 @@ export function useChat() {
|
|||||||
const [apiError, setApiError] = useState(null);
|
const [apiError, setApiError] = useState(null);
|
||||||
const activeContactRef = useRef(activeContactId);
|
const activeContactRef = useRef(activeContactId);
|
||||||
const contactsRef = useRef(contacts);
|
const contactsRef = useRef(contacts);
|
||||||
|
const isPaused = agentPresence?.status === 'paused';
|
||||||
|
|
||||||
const activeContact = useMemo(
|
const activeContact = useMemo(
|
||||||
() => {
|
() => {
|
||||||
@ -240,8 +269,12 @@ export function useChat() {
|
|||||||
const usersInTransferArea = accessUsers.filter((user) =>
|
const usersInTransferArea = accessUsers.filter((user) =>
|
||||||
user.areas?.some((area) => area.nome === transferArea) || user.areaPrincipal?.nome === transferArea,
|
user.areas?.some((area) => area.nome === transferArea) || user.areaPrincipal?.nome === transferArea,
|
||||||
);
|
);
|
||||||
|
const availableUsersInTransferArea = usersInTransferArea.filter((user) => {
|
||||||
|
const presence = getPresenceByUserId(presenceList, user.id);
|
||||||
|
return !presence || presence.status === 'available';
|
||||||
|
});
|
||||||
const isSameUserArea = currentUserAreas.includes(transferArea);
|
const isSameUserArea = currentUserAreas.includes(transferArea);
|
||||||
const attendants = isSameUserArea ? usersInTransferArea : [];
|
const attendants = isSameUserArea ? availableUsersInTransferArea : [];
|
||||||
const activeAssignment = activeContact?.assignment || null;
|
const activeAssignment = activeContact?.assignment || null;
|
||||||
const isAssignedToCurrentUser = Boolean(
|
const isAssignedToCurrentUser = Boolean(
|
||||||
activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId,
|
activeAssignment?.user_id && currentUserId && Number(activeAssignment.user_id) === currentUserId,
|
||||||
@ -251,8 +284,8 @@ export function useChat() {
|
|||||||
activeAssignment?.status === 'queued' &&
|
activeAssignment?.status === 'queued' &&
|
||||||
(isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
|
(isAdminUser || !activeAssignment.area_nome || currentUserAreas.includes(activeAssignment.area_nome)),
|
||||||
);
|
);
|
||||||
const canAssumeChat = Boolean(activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
|
const canAssumeChat = Boolean(!isPaused && activeContact?.id?.includes('@') && currentUserId && isQueuedForUserArea);
|
||||||
const canReply = Boolean(isAssignedToCurrentUser && !isWaitingCustomerReply);
|
const canReply = Boolean(!isPaused && isAssignedToCurrentUser && !isWaitingCustomerReply);
|
||||||
const assignmentLabel = activeAssignment?.user_id
|
const assignmentLabel = activeAssignment?.user_id
|
||||||
? isWaitingCustomerReply
|
? isWaitingCustomerReply
|
||||||
? 'Aguardando resposta do cliente para liberar novas mensagens'
|
? 'Aguardando resposta do cliente para liberar novas mensagens'
|
||||||
@ -268,7 +301,7 @@ export function useChat() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTransferAttendant(attendants[0]?.id ? String(attendants[0].id) : '');
|
setTransferAttendant(attendants[0]?.id ? String(attendants[0].id) : '');
|
||||||
}, [transferArea, accessUsers]);
|
}, [transferArea, accessUsers, presenceList]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
activeContactRef.current = activeContactId;
|
activeContactRef.current = activeContactId;
|
||||||
@ -283,14 +316,23 @@ export function useChat() {
|
|||||||
|
|
||||||
async function loadAccessData() {
|
async function loadAccessData() {
|
||||||
try {
|
try {
|
||||||
const [options, users] = await Promise.all([getAccessOptions(), getAccessUsers()]);
|
const [options, users, presences, currentPresence] = await Promise.all([
|
||||||
|
getAccessOptions(),
|
||||||
|
getAccessUsers(),
|
||||||
|
listAgentPresence(),
|
||||||
|
currentUserId ? getAgentPresence(currentUserId) : Promise.resolve(null),
|
||||||
|
]);
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setAreaOptions(options.areas || []);
|
setAreaOptions(options.areas || []);
|
||||||
setAccessUsers(users || []);
|
setAccessUsers(users || []);
|
||||||
|
setPresenceList(Array.isArray(presences) ? presences : []);
|
||||||
|
setAgentPresence(currentPresence);
|
||||||
|
setPauseSeconds(Number(currentPresence?.paused_seconds || 0));
|
||||||
} catch {
|
} catch {
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setAreaOptions([]);
|
setAreaOptions([]);
|
||||||
setAccessUsers([]);
|
setAccessUsers([]);
|
||||||
|
setPresenceList([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -299,7 +341,43 @@ export function useChat() {
|
|||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, [currentUserId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUserId) return undefined;
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
async function refreshPresence() {
|
||||||
|
try {
|
||||||
|
const [presences, currentPresence] = await Promise.all([
|
||||||
|
listAgentPresence(),
|
||||||
|
getAgentPresence(currentUserId),
|
||||||
|
]);
|
||||||
|
if (!isMounted) return;
|
||||||
|
setPresenceList(Array.isArray(presences) ? presences : []);
|
||||||
|
setAgentPresence(currentPresence);
|
||||||
|
setPauseSeconds(Number(currentPresence?.paused_seconds || 0));
|
||||||
|
} catch {
|
||||||
|
if (isMounted) setPresenceList([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(refreshPresence, 30000);
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [currentUserId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPaused) return undefined;
|
||||||
|
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
setPauseSeconds((current) => current + 1);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => window.clearInterval(intervalId);
|
||||||
|
}, [isPaused]);
|
||||||
|
|
||||||
function canSeeContact(contact) {
|
function canSeeContact(contact) {
|
||||||
if (isAdminUser) {
|
if (isAdminUser) {
|
||||||
@ -313,7 +391,15 @@ export function useChat() {
|
|||||||
return currentUserAreas.includes(contact.assignment.area_nome);
|
return currentUserAreas.includes(contact.assignment.area_nome);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChats({ showLoading = false } = {}) {
|
async function loadChats({ showLoading = false, ignorePause = false } = {}) {
|
||||||
|
if (isPaused && !ignorePause) {
|
||||||
|
setContacts((current) => (current.length ? [] : current));
|
||||||
|
setActiveContactId('');
|
||||||
|
setMessagesByContact({});
|
||||||
|
setIsLoadingChats(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (whatsappStatus !== 'CONNECTED') {
|
if (whatsappStatus !== 'CONNECTED') {
|
||||||
setContacts((current) => (current.length ? [] : current));
|
setContacts((current) => (current.length ? [] : current));
|
||||||
setActiveContactId('');
|
setActiveContactId('');
|
||||||
@ -358,7 +444,7 @@ export function useChat() {
|
|||||||
isMounted = false;
|
isMounted = false;
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [currentUserId, currentUserAreas.join('|'), isAdminUser, whatsappStatus]);
|
}, [currentUserId, currentUserAreas.join('|'), isAdminUser, isPaused, whatsappStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeContactId) return;
|
if (!activeContactId) return;
|
||||||
@ -586,6 +672,43 @@ export function useChat() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function pauseAttendance() {
|
||||||
|
if (!currentUserId) return;
|
||||||
|
setIsPresenceLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await pauseAgent(currentUserId);
|
||||||
|
setAgentPresence(result.presence);
|
||||||
|
setPauseSeconds(0);
|
||||||
|
setContacts([]);
|
||||||
|
setActiveContactId('');
|
||||||
|
setMessagesByContact({});
|
||||||
|
setIsTransferOpen(false);
|
||||||
|
setApiError(null);
|
||||||
|
} catch (error) {
|
||||||
|
setApiError(error.message);
|
||||||
|
} finally {
|
||||||
|
setIsPresenceLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeAttendance() {
|
||||||
|
if (!currentUserId) return;
|
||||||
|
setIsPresenceLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await resumeAgent(currentUserId);
|
||||||
|
setAgentPresence(result.presence);
|
||||||
|
setPauseSeconds(0);
|
||||||
|
setApiError(null);
|
||||||
|
await loadChats({ showLoading: true, ignorePause: true });
|
||||||
|
const presences = await listAgentPresence();
|
||||||
|
setPresenceList(Array.isArray(presences) ? presences : []);
|
||||||
|
} catch (error) {
|
||||||
|
setApiError(error.message);
|
||||||
|
} finally {
|
||||||
|
setIsPresenceLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendMessage(messageText = draft, contactId = activeContactId) {
|
async function sendMessage(messageText = draft, contactId = activeContactId) {
|
||||||
const rawMessage = typeof messageText === 'string' ? messageText : draft;
|
const rawMessage = typeof messageText === 'string' ? messageText : draft;
|
||||||
const trimmed = rawMessage.trim();
|
const trimmed = rawMessage.trim();
|
||||||
@ -769,5 +892,12 @@ export function useChat() {
|
|||||||
transferNote,
|
transferNote,
|
||||||
setTransferNote,
|
setTransferNote,
|
||||||
submitTransfer,
|
submitTransfer,
|
||||||
|
agentPresence,
|
||||||
|
isPaused,
|
||||||
|
pauseSeconds,
|
||||||
|
pauseDurationLabel: formatPauseDuration(pauseSeconds),
|
||||||
|
isPresenceLoading,
|
||||||
|
pauseAttendance,
|
||||||
|
resumeAttendance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,8 @@ export function ChatPage() {
|
|||||||
transferNote,
|
transferNote,
|
||||||
setTransferNote,
|
setTransferNote,
|
||||||
submitTransfer,
|
submitTransfer,
|
||||||
|
isPaused,
|
||||||
|
pauseDurationLabel,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
const requestedChatId = searchParams.get('chatId');
|
const requestedChatId = searchParams.get('chatId');
|
||||||
const handledRequestedChatIdRef = useRef('');
|
const handledRequestedChatIdRef = useRef('');
|
||||||
@ -111,7 +113,7 @@ export function ChatPage() {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Atendimento em tempo real
|
{isPaused ? `Pausado ha ${pauseDurationLabel}` : 'Atendimento em tempo real'}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to="/home"
|
to="/home"
|
||||||
@ -173,6 +175,8 @@ export function ChatPage() {
|
|||||||
assignmentLabel={assignmentLabel}
|
assignmentLabel={assignmentLabel}
|
||||||
transferNote={transferNoteLabel}
|
transferNote={transferNoteLabel}
|
||||||
isReplying={isReplying}
|
isReplying={isReplying}
|
||||||
|
isPaused={isPaused}
|
||||||
|
pauseDurationLabel={pauseDurationLabel}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -188,6 +192,7 @@ export function ChatPage() {
|
|||||||
key={reply}
|
key={reply}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDraft(reply)}
|
onClick={() => setDraft(reply)}
|
||||||
|
disabled={isPaused}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: '18px',
|
borderRadius: '18px',
|
||||||
@ -196,6 +201,7 @@ export function ChatPage() {
|
|||||||
color: 'var(--color-primary)',
|
color: 'var(--color-primary)',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
|
opacity: isPaused ? 0.55 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reply}
|
{reply}
|
||||||
|
|||||||
46
src/modules/chat/services/agentPresenceService.js
Normal file
46
src/modules/chat/services/agentPresenceService.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { API_BASE_URL } from '../../../shared/services/apiConfig';
|
||||||
|
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Falha ao atualizar presenca do agente');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentPresence(userId) {
|
||||||
|
return request(`/agent/presence/me?userId=${encodeURIComponent(userId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listAgentPresence() {
|
||||||
|
return request('/agent/presence');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pauseAgent(userId) {
|
||||||
|
return request('/agent/presence/pause', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resumeAgent(userId) {
|
||||||
|
return request('/agent/presence/resume', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markAgentOffline(userId) {
|
||||||
|
return request('/agent/presence/offline', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,29 +1,39 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function AttendantOpsPanel({ activeChatsCount }) {
|
export function AttendantOpsPanel({
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
activeChatsCount,
|
||||||
|
isPaused = false,
|
||||||
|
pauseDurationLabel = '00:00',
|
||||||
|
isPresenceLoading = false,
|
||||||
|
onTogglePause,
|
||||||
|
}) {
|
||||||
const [secondsOnline, setSecondsOnline] = useState(0);
|
const [secondsOnline, setSecondsOnline] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval;
|
if (isPaused) return undefined;
|
||||||
if (!isPaused) {
|
|
||||||
interval = setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
setSecondsOnline((s) => s + 1);
|
setSecondsOnline((current) => current + 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
|
||||||
return () => clearInterval(interval);
|
return () => window.clearInterval(intervalId);
|
||||||
}, [isPaused]);
|
}, [isPaused]);
|
||||||
|
|
||||||
const formatTime = (totalSeconds) => {
|
const formatTime = (totalSeconds) => {
|
||||||
const h = Math.floor(totalSeconds / 3600);
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
const m = Math.floor((totalSeconds % 3600) / 60);
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
const s = totalSeconds % 60;
|
const seconds = totalSeconds % 60;
|
||||||
return [h, m, s]
|
|
||||||
.map(v => v.toString().padStart(2, '0'))
|
return [hours, minutes, seconds]
|
||||||
.filter((v, i) => v !== '00' || i > 0)
|
.map((value) => value.toString().padStart(2, '0'))
|
||||||
|
.filter((value, index) => value !== '00' || index > 0)
|
||||||
.join(':');
|
.join(':');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const presenceLabel = isPaused ? 'Tempo em pausa' : 'Tempo online';
|
||||||
|
const presenceTime = isPaused ? pauseDurationLabel : formatTime(secondsOnline);
|
||||||
|
const statusColor = isPaused ? '#ef4444' : '#10b981';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -46,20 +56,22 @@ export function AttendantOpsPanel({ activeChatsCount }) {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem', fontWeight: 600 }}>
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem', fontWeight: 600 }}>
|
||||||
Tempo Online
|
{presenceLabel}
|
||||||
</span>
|
</span>
|
||||||
<strong style={{ display: 'block', fontSize: '1.6rem', marginTop: '0.2rem', color: 'var(--color-text)' }}>
|
<strong style={{ display: 'block', fontSize: '1.6rem', marginTop: '0.2rem', color: 'var(--color-text)' }}>
|
||||||
{formatTime(secondsOnline)}
|
{presenceTime}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div
|
||||||
width: '12px',
|
title={isPaused ? 'Agente pausado' : 'Agente disponivel'}
|
||||||
height: '12px',
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: isPaused ? '#ef4444' : '#10b981',
|
background: statusColor,
|
||||||
boxShadow: `0 0 10px ${isPaused ? '#ef4444' : '#10b981'}`,
|
boxShadow: `0 0 10px ${statusColor}`,
|
||||||
animation: !isPaused ? 'pulse 2s infinite' : 'none'
|
}}
|
||||||
}} />
|
/>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
@ -76,10 +88,10 @@ export function AttendantOpsPanel({ activeChatsCount }) {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem', fontWeight: 600 }}>
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.9rem', fontWeight: 600 }}>
|
||||||
Atendimentos Abertos
|
Atendimentos abertos
|
||||||
</span>
|
</span>
|
||||||
<strong style={{ display: 'block', fontSize: '1.6rem', marginTop: '0.2rem', color: 'var(--color-text)' }}>
|
<strong style={{ display: 'block', fontSize: '1.6rem', marginTop: '0.2rem', color: 'var(--color-text)' }}>
|
||||||
{activeChatsCount}
|
{isPaused ? 0 : activeChatsCount}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@ -93,11 +105,13 @@ export function AttendantOpsPanel({ activeChatsCount }) {
|
|||||||
boxShadow: 'var(--shadow-sm)',
|
boxShadow: 'var(--shadow-sm)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsPaused(!isPaused)}
|
type="button"
|
||||||
|
onClick={onTogglePause}
|
||||||
|
disabled={isPresenceLoading}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -107,12 +121,13 @@ export function AttendantOpsPanel({ activeChatsCount }) {
|
|||||||
background: isPaused ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
|
background: isPaused ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
|
||||||
color: isPaused ? '#10b981' : '#ef4444',
|
color: isPaused ? '#10b981' : '#ef4444',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
fontWeight: 700,
|
fontWeight: 800,
|
||||||
cursor: 'pointer',
|
cursor: isPresenceLoading ? 'wait' : 'pointer',
|
||||||
transition: 'all 0.2s ease',
|
transition: 'all 0.2s ease',
|
||||||
|
opacity: isPresenceLoading ? 0.7 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isPaused ? '▶ Retomar Atendimento' : '⏸ Pausar'}
|
{isPaused ? 'Retomar Atendimento' : 'Pausar'}
|
||||||
</button>
|
</button>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -200,6 +200,8 @@ export function MessagesWorkspace({
|
|||||||
isDesktop = false,
|
isDesktop = false,
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
|
isPaused = false,
|
||||||
|
pauseDurationLabel = '00:00',
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const messagesRef = useRef(null);
|
const messagesRef = useRef(null);
|
||||||
@ -311,6 +313,7 @@ export function MessagesWorkspace({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendSuggestedReply() {
|
async function sendSuggestedReply() {
|
||||||
|
if (isPaused) return;
|
||||||
if (!safeActiveConversation.id || safeActiveConversation.id === 'empty') return;
|
if (!safeActiveConversation.id || safeActiveConversation.id === 'empty') return;
|
||||||
|
|
||||||
await onSendSuggestedReply?.(safeActiveConversation.id, selectedReply);
|
await onSendSuggestedReply?.(safeActiveConversation.id, selectedReply);
|
||||||
@ -393,7 +396,10 @@ export function MessagesWorkspace({
|
|||||||
{conversations.length > 3 ? (
|
{conversations.length > 3 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate('/chat')}
|
onClick={() => {
|
||||||
|
if (!isPaused) navigate('/chat');
|
||||||
|
}}
|
||||||
|
disabled={isPaused}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
@ -401,6 +407,8 @@ export function MessagesWorkspace({
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
color: 'var(--color-primary)',
|
color: 'var(--color-primary)',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
opacity: isPaused ? 0.55 : 1,
|
||||||
|
cursor: isPaused ? 'not-allowed' : 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Ver todos no chat
|
Ver todos no chat
|
||||||
@ -442,7 +450,10 @@ export function MessagesWorkspace({
|
|||||||
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate('/chat')}
|
onClick={() => {
|
||||||
|
if (!isPaused) navigate('/chat');
|
||||||
|
}}
|
||||||
|
disabled={isPaused}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: '14px',
|
borderRadius: '14px',
|
||||||
@ -450,6 +461,8 @@ export function MessagesWorkspace({
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
color: 'var(--color-primary)',
|
color: 'var(--color-primary)',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
opacity: isPaused ? 0.55 : 1,
|
||||||
|
cursor: isPaused ? 'not-allowed' : 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Abrir chat
|
Abrir chat
|
||||||
@ -571,12 +584,15 @@ export function MessagesWorkspace({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={selectPreviousReply}
|
onClick={selectPreviousReply}
|
||||||
title="Resposta anterior"
|
title="Resposta anterior"
|
||||||
|
disabled={isPaused}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: '14px',
|
borderRadius: '14px',
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
color: 'var(--color-primary)',
|
color: 'var(--color-primary)',
|
||||||
fontWeight: 900,
|
fontWeight: 900,
|
||||||
|
opacity: isPaused ? 0.55 : 1,
|
||||||
|
cursor: isPaused ? 'not-allowed' : 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
‹
|
‹
|
||||||
@ -584,6 +600,7 @@ export function MessagesWorkspace({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={sendSuggestedReply}
|
onClick={sendSuggestedReply}
|
||||||
|
disabled={isPaused}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid rgba(0, 164, 183, 0.32)',
|
border: '1px solid rgba(0, 164, 183, 0.32)',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
@ -598,9 +615,11 @@ export function MessagesWorkspace({
|
|||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
WebkitLineClamp: 2,
|
WebkitLineClamp: 2,
|
||||||
WebkitBoxOrient: 'vertical',
|
WebkitBoxOrient: 'vertical',
|
||||||
|
opacity: isPaused ? 0.55 : 1,
|
||||||
|
cursor: isPaused ? 'not-allowed' : 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedReply}
|
{isPaused ? `Voce esta em pausa ha ${pauseDurationLabel}. Retome para responder.` : selectedReply}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -37,6 +37,11 @@ export function HomePage() {
|
|||||||
messages,
|
messages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
isLoadingChats,
|
isLoadingChats,
|
||||||
|
isPaused,
|
||||||
|
pauseDurationLabel,
|
||||||
|
isPresenceLoading,
|
||||||
|
pauseAttendance,
|
||||||
|
resumeAttendance,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
const [activeTab, setActiveTab] = useState('messages');
|
const [activeTab, setActiveTab] = useState('messages');
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
@ -122,7 +127,13 @@ export function HomePage() {
|
|||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AttendantOpsPanel activeChatsCount={filteredConversations.length} />
|
<AttendantOpsPanel
|
||||||
|
activeChatsCount={filteredConversations.length}
|
||||||
|
isPaused={isPaused}
|
||||||
|
pauseDurationLabel={pauseDurationLabel}
|
||||||
|
isPresenceLoading={isPresenceLoading}
|
||||||
|
onTogglePause={isPaused ? resumeAttendance : pauseAttendance}
|
||||||
|
/>
|
||||||
|
|
||||||
{isLoadingChats ? (
|
{isLoadingChats ? (
|
||||||
<div
|
<div
|
||||||
@ -152,6 +163,8 @@ export function HomePage() {
|
|||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
isPaused={isPaused}
|
||||||
|
pauseDurationLabel={pauseDurationLabel}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CallsWorkspace
|
<CallsWorkspace
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user