FEAT: Ajusta home page do atendente
- Adicionado aba de comunicados e notas - Alterado aba lateral para exibir apenas as opções de atendimento - Removido arquivos de build do repositório
This commit is contained in:
parent
de1e4f518b
commit
2229a29af1
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,4 +1,31 @@
|
||||
node_modules
|
||||
dist
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.development.local
|
||||
.env.production
|
||||
.env.production.local
|
||||
.env.test
|
||||
.env.test.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Vite cache
|
||||
.vite/
|
||||
|
||||
# Editor and OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
BIN
dist/assets/favicon_blue-CzkOczz3.png
vendored
BIN
dist/assets/favicon_blue-CzkOczz3.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 KiB |
68
dist/assets/index-1xjqdjIG.js
vendored
68
dist/assets/index-1xjqdjIG.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-BsY34Fgu.css
vendored
1
dist/assets/index-BsY34Fgu.css
vendored
@ -1 +0,0 @@
|
||||
:root{font-family:Segoe UI,Helvetica Neue,sans-serif;color:#122230;background:radial-gradient(circle at top left,rgba(0,164,183,.12),transparent 28%),radial-gradient(circle at bottom right,rgba(229,162,42,.14),transparent 24%),#f5f8fb;color-scheme:light;--color-primary: #003150;--color-secondary: #b51f1f;--color-accent: #00a4b7;--color-highlight: #e5a22a;--color-surface: rgba(255, 255, 255, .9);--color-surface-strong: #ffffff;--color-text: #122230;--color-text-soft: #5e6d7b;--color-border: rgba(0, 49, 80, .12);--shadow-lg: 0 24px 60px rgba(0, 49, 80, .12);--shadow-md: 0 12px 28px rgba(0, 49, 80, .08)}*{box-sizing:border-box}html,body,#root{min-height:100%;margin:0}body{min-height:100vh}body,button,input{font:inherit}button{cursor:pointer}a{color:inherit;text-decoration:none}
|
||||
BIN
dist/assets/logo_white_dark_mode-BKcVSu03.png
vendored
BIN
dist/assets/logo_white_dark_mode-BKcVSu03.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
BIN
dist/assets/logo_white_mode-BIHgqUPv.png
vendored
BIN
dist/assets/logo_white_mode-BIHgqUPv.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
14
dist/index.html
vendored
14
dist/index.html
vendored
@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/assets/favicon_blue-CzkOczz3.png" />
|
||||
<title>Omnichannel Sothis</title>
|
||||
<script type="module" crossorigin src="/assets/index-1xjqdjIG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BsY34Fgu.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@ -39,7 +39,7 @@ const initialFormData = {
|
||||
|
||||
export function LoginForm() {
|
||||
const [formData, setFormData] = useState(initialFormData);
|
||||
const { login, isSubmitting } = useLogin();
|
||||
const { login, startMicrosoftLogin, providers, error, isSubmitting } = useLogin();
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
@ -48,12 +48,15 @@ export function LoginForm() {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: '1rem' }}>
|
||||
{providers.ldap ? (
|
||||
<>
|
||||
<label style={{ display: 'grid', gap: '0.5rem' }}>
|
||||
<span style={{ fontWeight: 600 }}>Usuário AD</span>
|
||||
<span style={{ fontWeight: 600 }}>Usuario AD</span>
|
||||
<input
|
||||
style={fieldStyle}
|
||||
type="text"
|
||||
placeholder="seu.usuario"
|
||||
autoComplete="username"
|
||||
value={formData.username}
|
||||
onChange={(event) =>
|
||||
setFormData((current) => ({ ...current, username: event.target.value }))
|
||||
@ -67,6 +70,7 @@ export function LoginForm() {
|
||||
style={fieldStyle}
|
||||
type="password"
|
||||
placeholder="Digite sua senha"
|
||||
autoComplete="current-password"
|
||||
value={formData.password}
|
||||
onChange={(event) =>
|
||||
setFormData((current) => ({ ...current, password: event.target.value }))
|
||||
@ -75,23 +79,58 @@ export function LoginForm() {
|
||||
</label>
|
||||
|
||||
<button style={primaryButtonStyle} type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Entrando...' : 'Entrar'}
|
||||
{isSubmitting ? 'Entrando...' : 'Entrar com AD'}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<button style={secondaryButtonStyle} type="button">
|
||||
{providers.microsoft ? (
|
||||
<button style={secondaryButtonStyle} type="button" onClick={startMicrosoftLogin}>
|
||||
Entrar com Microsoft
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<a
|
||||
href="#forgot-password"
|
||||
{error ? (
|
||||
<div
|
||||
role="alert"
|
||||
style={{
|
||||
border: '1px solid rgba(180, 35, 24, 0.24)',
|
||||
borderRadius: 14,
|
||||
padding: '0.85rem 1rem',
|
||||
background: 'rgba(180, 35, 24, 0.08)',
|
||||
color: '#b42318',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!providers.ldap && !providers.microsoft ? (
|
||||
<div
|
||||
role="alert"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 14,
|
||||
padding: '0.85rem 1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Nenhum provedor de login esta habilitado.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<span
|
||||
style={{
|
||||
justifySelf: 'center',
|
||||
color: 'var(--color-secondary)',
|
||||
color: 'var(--color-text-soft)',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Esqueci minha senha
|
||||
</a>
|
||||
Acesso somente via AD ou Microsoft corporativo.
|
||||
</span>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,17 +1,51 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { mockLogin } from '../services/authService';
|
||||
import {
|
||||
getAuthConfig,
|
||||
loginWithAd,
|
||||
startMicrosoftLogin,
|
||||
storeAuthSession,
|
||||
} from '../services/authService';
|
||||
|
||||
export function useLogin() {
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [providers, setProviders] = useState({ ldap: true, microsoft: false });
|
||||
|
||||
async function login() {
|
||||
setIsSubmitting(true);
|
||||
useEffect(() => {
|
||||
getAuthConfig()
|
||||
.then((config) => setProviders(config.providers || { ldap: true, microsoft: false }))
|
||||
.catch(() => setProviders({ ldap: true, microsoft: false }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
const rawUser = params.get('user');
|
||||
|
||||
if (!token || !rawUser) return;
|
||||
|
||||
try {
|
||||
await mockLogin();
|
||||
const user = JSON.parse(rawUser);
|
||||
storeAuthSession({ token, user });
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
navigate('/home', { replace: true });
|
||||
} catch {
|
||||
setError('Nao foi possivel concluir o login Microsoft.');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
async function login(credentials) {
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const authResult = await loginWithAd(credentials);
|
||||
storeAuthSession(authResult);
|
||||
navigate('/home');
|
||||
} catch (loginError) {
|
||||
setError(loginError.message || 'Falha ao autenticar.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
@ -19,6 +53,9 @@ export function useLogin() {
|
||||
|
||||
return {
|
||||
isSubmitting,
|
||||
error,
|
||||
providers,
|
||||
login,
|
||||
startMicrosoftLogin,
|
||||
};
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export function LoginPage() {
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
MVP de atendimento
|
||||
Atendimento Múltiplos canais
|
||||
</p>
|
||||
<h1
|
||||
style={{
|
||||
@ -67,7 +67,7 @@ export function LoginPage() {
|
||||
lineHeight: 1.05,
|
||||
}}
|
||||
>
|
||||
Conecte-se com seu cliente em uma única tela.
|
||||
Conexão multiatendimento em um único lugar.
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
@ -91,7 +91,7 @@ export function LoginPage() {
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ label: 'Canais', value: 'WhatsApp, SMS e Voz' },
|
||||
{ label: 'Canais', value: 'WhatsApp, SMS e E-mail' },
|
||||
{ label: 'Fila', value: 'Distribuição rápida' },
|
||||
{ label: 'UX', value: 'Padrão SaaS responsivo' },
|
||||
].map((item) => (
|
||||
@ -147,8 +147,7 @@ export function LoginPage() {
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Use seu usuário corporativo para acessar o MVP. A autenticação e mockada
|
||||
nesta etapa e leva você diretamente para a dashboard principal.
|
||||
Use seu usuario corporativo para acessar o MVP com Active Directory ou Microsoft.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,35 @@
|
||||
const networkDelay = 450;
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
|
||||
|
||||
export async function mockLogin() {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, networkDelay));
|
||||
async function parseJsonResponse(response) {
|
||||
const data = await response.json().catch(() => null);
|
||||
|
||||
return {
|
||||
id: 'agent-001',
|
||||
name: 'Ana Camolesi',
|
||||
email: 'ana.camolesi@sothis.local',
|
||||
};
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message || 'Nao foi possivel autenticar.');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getAuthConfig() {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/config`);
|
||||
return parseJsonResponse(response);
|
||||
}
|
||||
|
||||
export async function loginWithAd(credentials) {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
return parseJsonResponse(response);
|
||||
}
|
||||
|
||||
export function startMicrosoftLogin() {
|
||||
window.location.href = `${API_BASE_URL}/auth/oauth/microsoft/start`;
|
||||
}
|
||||
|
||||
export function storeAuthSession(authResult) {
|
||||
window.localStorage.setItem('authToken', authResult.token);
|
||||
window.localStorage.setItem('authUser', JSON.stringify(authResult.user));
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { clearSession, getCurrentUserProfile } from '../../auth/services/sessionService';
|
||||
import { clearSession } from '../../auth/services/sessionService';
|
||||
|
||||
export function HomeSidebar({ items, activeItem, isMobile = false }) {
|
||||
const navigate = useNavigate();
|
||||
@ -29,7 +29,7 @@ export function HomeSidebar({ items, activeItem, isMobile = false }) {
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
+ Novo Atendimento
|
||||
Abrir atendimento
|
||||
</button>
|
||||
|
||||
<nav
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
|
||||
function formatCurrentDateTime(date) {
|
||||
return new Intl.DateTimeFormat('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function HomeTopbar({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
@ -8,19 +21,29 @@ export function HomeTopbar({
|
||||
isTablet = false,
|
||||
isMobile = false,
|
||||
}) {
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [currentDateTime, setCurrentDateTime] = useState(() => formatCurrentDateTime(new Date()));
|
||||
const tabs = [
|
||||
{ id: 'messages', label: 'Mensagens' },
|
||||
{ id: 'calls', label: 'Ligações' },
|
||||
{ id: 'calls', label: 'Ligacoes' },
|
||||
];
|
||||
|
||||
const gridTemplateColumns = isMobile
|
||||
? '1fr'
|
||||
: isWideDesktop
|
||||
? 'max-content minmax(180px, 220px) minmax(280px, 1fr) max-content'
|
||||
? 'max-content minmax(150px, 190px) minmax(280px, 1fr) max-content'
|
||||
: isDesktop || isTablet
|
||||
? 'repeat(2, minmax(0, 1fr))'
|
||||
: '1fr';
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = window.setInterval(() => {
|
||||
setCurrentDateTime(formatCurrentDateTime(new Date()));
|
||||
}, 1000);
|
||||
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header
|
||||
style={{
|
||||
@ -75,9 +98,13 @@ export function HomeTopbar({
|
||||
fontWeight: 600,
|
||||
width: isMobile ? '100%' : 'auto',
|
||||
minWidth: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Sexta, 19 de março
|
||||
{currentDateTime}
|
||||
</div>
|
||||
|
||||
<input
|
||||
@ -108,9 +135,9 @@ export function HomeTopbar({
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'right', minWidth: 0 }}>
|
||||
<strong style={{ display: 'block' }}>Ana Camolesi</strong>
|
||||
<strong style={{ display: 'block' }}>{userDisplay.name}</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||
Atendimento omnichannel
|
||||
{userDisplay.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@ -126,7 +153,7 @@ export function HomeTopbar({
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
AM
|
||||
{userDisplay.initials}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const WORKSPACE_HEIGHT = 660;
|
||||
|
||||
function ChannelBadge({ channel }) {
|
||||
const colors = {
|
||||
WhatsApp: '#2bb741',
|
||||
@ -25,20 +28,135 @@ function ChannelBadge({ channel }) {
|
||||
);
|
||||
}
|
||||
|
||||
function buildSuggestedReplies(conversation) {
|
||||
const lastMessage = conversation?.lastMessage || conversation?.messages?.at(-1)?.text || '';
|
||||
const firstName = conversation?.name?.split(' ')?.[0] || 'voce';
|
||||
const lowerContext = lastMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerContext.includes('fatura') ||
|
||||
lowerContext.includes('cobranca') ||
|
||||
lowerContext.includes('pagamento')
|
||||
) {
|
||||
return [
|
||||
`${firstName}, vou conferir os dados financeiros e ja te retorno com a posicao correta.`,
|
||||
'Recebi sua mensagem sobre cobranca. Vou validar o historico antes de seguir com a orientacao.',
|
||||
'Consigo te ajudar com isso. Pode me confirmar o CPF/CNPJ ou protocolo vinculado ao atendimento?',
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
lowerContext.includes('endereco') ||
|
||||
lowerContext.includes('cadastro') ||
|
||||
lowerContext.includes('atualizar')
|
||||
) {
|
||||
return [
|
||||
`${firstName}, vou validar seu cadastro e confirmar se a alteracao ja foi registrada.`,
|
||||
'Para seguir com a atualizacao, me confirme por favor os dados que precisam ser ajustados.',
|
||||
'Entendi. Vou verificar o cadastro atual e te retorno com o proximo passo.',
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
lowerContext.includes('ligar') ||
|
||||
lowerContext.includes('telefone') ||
|
||||
lowerContext.includes('retorno')
|
||||
) {
|
||||
return [
|
||||
`${firstName}, consigo organizar esse retorno. Qual o melhor horario para contato?`,
|
||||
'Vou registrar sua solicitacao e direcionar o retorno para o time responsavel.',
|
||||
'Obrigado pelo aviso. Vou confirmar disponibilidade e te retorno por aqui.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
`${firstName}, recebi sua mensagem e vou verificar o contexto para te orientar corretamente.`,
|
||||
'Entendi. Vou analisar as informacoes do atendimento e retorno com o melhor encaminhamento.',
|
||||
'Posso acionar o time responsavel e te atualizar por aqui assim que tiver uma posicao.',
|
||||
];
|
||||
}
|
||||
|
||||
export function MessagesWorkspace({
|
||||
conversations,
|
||||
activeConversationId,
|
||||
onSelectConversation,
|
||||
actionItems,
|
||||
isWideDesktop = false,
|
||||
isDesktop = false,
|
||||
isTablet = false,
|
||||
isMobile = false,
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const recentConversations = conversations.slice(0, 3);
|
||||
const activeConversation =
|
||||
conversations.find((conversation) => conversation.id === activeConversationId) ||
|
||||
recentConversations.find((conversation) => conversation.id === activeConversationId) ||
|
||||
recentConversations[0] ||
|
||||
conversations[0];
|
||||
const safeActiveConversation = activeConversation || {
|
||||
id: 'empty',
|
||||
name: 'Nenhuma conversa',
|
||||
status: 'offline',
|
||||
messages: [],
|
||||
};
|
||||
const suggestedReplies = useMemo(
|
||||
() => buildSuggestedReplies(safeActiveConversation),
|
||||
[safeActiveConversation],
|
||||
);
|
||||
const [selectedReplyIndex, setSelectedReplyIndex] = useState(0);
|
||||
const [noteDraft, setNoteDraft] = useState('');
|
||||
const [notes, setNotes] = useState(() => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem('agentNotes') || '[]');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const selectedReply = suggestedReplies[selectedReplyIndex] || suggestedReplies[0];
|
||||
const managerMessages = [
|
||||
{
|
||||
id: 'sla',
|
||||
title: 'Comunicado do supervisor',
|
||||
text: 'Priorizar atendimentos com SLA abaixo de 15 minutos antes de abrir novos casos.',
|
||||
},
|
||||
{
|
||||
id: 'script',
|
||||
title: 'Atualizacao de script',
|
||||
text: 'Use o novo roteiro de confirmacao de dados em atendimentos financeiros.',
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedReplyIndex(0);
|
||||
}, [safeActiveConversation.id]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem('agentNotes', JSON.stringify(notes));
|
||||
}, [notes]);
|
||||
|
||||
function selectPreviousReply() {
|
||||
setSelectedReplyIndex((current) =>
|
||||
current === 0 ? suggestedReplies.length - 1 : current - 1,
|
||||
);
|
||||
}
|
||||
|
||||
function selectNextReply() {
|
||||
setSelectedReplyIndex((current) => (current + 1) % suggestedReplies.length);
|
||||
}
|
||||
|
||||
function saveNote() {
|
||||
const text = noteDraft.trim();
|
||||
if (!text) return;
|
||||
|
||||
setNotes((current) => [
|
||||
{
|
||||
id: Date.now(),
|
||||
text,
|
||||
time: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }),
|
||||
},
|
||||
...current,
|
||||
]);
|
||||
setNoteDraft('');
|
||||
}
|
||||
|
||||
const gridTemplateColumns = isMobile
|
||||
? '1fr'
|
||||
@ -48,13 +166,15 @@ export function MessagesWorkspace({
|
||||
? 'minmax(260px, 320px) minmax(0, 1fr)'
|
||||
: '1fr';
|
||||
|
||||
const panelHeight = isMobile ? 'auto' : WORKSPACE_HEIGHT;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns,
|
||||
gap: '1rem',
|
||||
alignItems: 'start',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
<section
|
||||
@ -65,18 +185,20 @@ export function MessagesWorkspace({
|
||||
padding: '1rem',
|
||||
display: 'grid',
|
||||
gap: '0.75rem',
|
||||
alignContent: 'start',
|
||||
height: panelHeight,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ fontSize: '1.05rem' }}>Conversas</strong>
|
||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Atendimento em tempo real por canal.
|
||||
Ultimos 3 atendimentos em tempo real.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{conversations.map((conversation) => {
|
||||
const isActive = conversation.id === activeConversation.id;
|
||||
{recentConversations.map((conversation) => {
|
||||
const isActive = conversation.id === safeActiveConversation.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -123,6 +245,23 @@ export function MessagesWorkspace({
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{conversations.length > 3 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/chat')}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '16px',
|
||||
padding: '0.85rem 1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
Ver todos no chat
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section
|
||||
@ -131,8 +270,9 @@ export function MessagesWorkspace({
|
||||
borderRadius: '26px',
|
||||
border: '1px solid var(--color-border)',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto 1fr auto',
|
||||
minHeight: 580,
|
||||
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
||||
height: panelHeight,
|
||||
minHeight: isMobile ? 580 : 'auto',
|
||||
overflow: 'hidden',
|
||||
minWidth: 0,
|
||||
}}
|
||||
@ -148,9 +288,11 @@ export function MessagesWorkspace({
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ display: 'block', fontSize: '1.08rem' }}>{activeConversation.name}</strong>
|
||||
<strong style={{ display: 'block', fontSize: '1.08rem' }}>
|
||||
{safeActiveConversation.name}
|
||||
</strong>
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||
{activeConversation.status === 'online' ? 'Online agora' : 'Offline'}
|
||||
{safeActiveConversation.status === 'online' ? 'Online agora' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||
@ -190,11 +332,12 @@ export function MessagesWorkspace({
|
||||
display: 'grid',
|
||||
gap: '0.9rem',
|
||||
alignContent: 'start',
|
||||
overflowY: 'auto',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
|
||||
}}
|
||||
>
|
||||
{activeConversation.messages.map((message) => {
|
||||
{safeActiveConversation.messages.map((message) => {
|
||||
const isAgent = message.from === 'agent';
|
||||
|
||||
return (
|
||||
@ -218,37 +361,72 @@ export function MessagesWorkspace({
|
||||
|
||||
<footer
|
||||
style={{
|
||||
padding: '1rem 1.25rem 1.25rem',
|
||||
padding: '0.85rem 1.25rem 1rem',
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gap: '0.75rem',
|
||||
gap: '0.65rem',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="Posso acionar o time responsavel e te retorno em seguida."
|
||||
readOnly
|
||||
<strong style={{ display: 'block', fontSize: '0.94rem' }}>Resposta sugerida</strong>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '40px minmax(0, 1fr) 40px',
|
||||
gap: '0.6rem',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={selectPreviousReply}
|
||||
title="Resposta anterior"
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1.2rem',
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 700,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '14px',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
Enviar
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/chat')}
|
||||
style={{
|
||||
border: '1px solid rgba(0, 164, 183, 0.32)',
|
||||
borderRadius: '16px',
|
||||
padding: '0.75rem 0.9rem',
|
||||
background: 'rgba(0, 164, 183, 0.07)',
|
||||
color: 'var(--color-text)',
|
||||
fontWeight: 600,
|
||||
textAlign: 'left',
|
||||
lineHeight: 1.35,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{selectedReply}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={selectNextReply}
|
||||
title="Proxima resposta"
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '14px',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 900,
|
||||
}}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
@ -259,49 +437,102 @@ export function MessagesWorkspace({
|
||||
border: '1px solid var(--color-border)',
|
||||
padding: '1.2rem',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||
gap: '1rem',
|
||||
alignContent: 'start',
|
||||
gridColumn: isWideDesktop ? 'auto' : '1 / -1',
|
||||
height: panelHeight,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<strong style={{ fontSize: '1.05rem' }}>Painel de ações</strong>
|
||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
||||
Contexto rápido do atendimento selecionado.
|
||||
</p>
|
||||
<strong style={{ fontSize: '1.05rem' }}>Comunicados e notas</strong>
|
||||
</div>
|
||||
|
||||
{actionItems.map((item) => (
|
||||
<article
|
||||
key={item.title}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: '20px',
|
||||
padding: '1rem',
|
||||
background: 'rgba(0, 49, 80, 0.04)',
|
||||
display: 'grid',
|
||||
gap: '0.85rem',
|
||||
alignContent: 'start',
|
||||
overflowY: 'auto',
|
||||
paddingRight: '0.15rem',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)', display: 'block', marginBottom: '0.35rem' }}>
|
||||
{item.title}
|
||||
</span>
|
||||
<strong>{item.value}</strong>
|
||||
{managerMessages.map((message) => (
|
||||
<article
|
||||
key={message.id}
|
||||
style={{
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem',
|
||||
background: 'rgba(0, 49, 80, 0.04)',
|
||||
display: 'grid',
|
||||
gap: '0.4rem',
|
||||
}}
|
||||
>
|
||||
<strong>{message.title}</strong>
|
||||
<p style={{ margin: 0, color: 'var(--color-text-soft)', lineHeight: 1.5 }}>
|
||||
{message.text}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/new-attendance')}
|
||||
<label style={{ display: 'grid', gap: '0.45rem' }}>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontWeight: 700 }}>Anotacao rapida</span>
|
||||
<textarea
|
||||
value={noteDraft}
|
||||
onChange={(event) => setNoteDraft(event.target.value)}
|
||||
placeholder="Ex: cliente pediu retorno apos as 15h"
|
||||
rows={4}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '14px',
|
||||
padding: '0.85rem 0.9rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-text)',
|
||||
resize: 'none',
|
||||
outline: 'none',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={saveNote}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '18px',
|
||||
padding: '0.95rem 1rem',
|
||||
background: '#fff',
|
||||
color: 'var(--color-primary)',
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||
color: '#fff',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
Criar novo fluxo
|
||||
Salvar anotacao
|
||||
</button>
|
||||
|
||||
<div style={{ display: 'grid', gap: '0.55rem' }}>
|
||||
{notes.length ? (
|
||||
notes.map((note) => (
|
||||
<article
|
||||
key={note.id}
|
||||
style={{
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '16px',
|
||||
padding: '0.8rem',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.82rem' }}>
|
||||
{note.time}
|
||||
</span>
|
||||
<p style={{ margin: '0.35rem 0 0', lineHeight: 1.45 }}>{note.text}</p>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<span style={{ color: 'var(--color-text-soft)' }}>Nenhuma anotacao salva.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { HomeTopbar } from '../components/HomeTopbar';
|
||||
import { MessagesWorkspace } from '../components/MessagesWorkspace';
|
||||
import { CallsWorkspace } from '../components/CallsWorkspace';
|
||||
import { AttendantOpsPanel } from '../components/AttendantOpsPanel';
|
||||
import { actionItems, recentCalls, sidebarItems } from '../services/homeMocks';
|
||||
import { recentCalls, sidebarItems } from '../services/homeMocks';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { useChat } from '../../chat/hooks/useChat';
|
||||
|
||||
@ -141,7 +141,6 @@ export function HomePage() {
|
||||
conversations={filteredConversations}
|
||||
activeConversationId={safeConversationId}
|
||||
onSelectConversation={setActiveContactId}
|
||||
actionItems={actionItems}
|
||||
isWideDesktop={isWideDesktop}
|
||||
isDesktop={isDesktop}
|
||||
isTablet={isTablet}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
export const sidebarItems = [
|
||||
{ id: 'dashboard', label: 'Dashboard' },
|
||||
{ id: 'new-attendance', label: 'Novos Atendimentos', route: '/new-attendance' },
|
||||
{ id: 'in-progress', label: 'Em andamento', count: 8 },
|
||||
{ id: 'completed', label: 'Finalizados', count: 24 },
|
||||
{ id: 'contacts', label: 'Contatos', count: 128 },
|
||||
{ id: 'scripts', label: 'Scripts e respostas prontas' },
|
||||
{ id: 'personal-reports', label: 'Relatorios pessoais' },
|
||||
{ id: 'mass-message', label: 'Disparo em massa' },
|
||||
{ id: 'knowledge-base', label: 'Base de conhecimento' },
|
||||
];
|
||||
|
||||
export const conversations = [
|
||||
|
||||
@ -91,7 +91,7 @@ export function ManagementLayout({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(activeSection === 'admin' ? '/admin' : '/supervisor')}
|
||||
onClick={() => navigate('/home')}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderRadius: '20px',
|
||||
|
||||
@ -6,6 +6,7 @@ import { MetricGrid } from '../components/MetricGrid';
|
||||
import { adminMetrics, aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
||||
import { getAccessOptions, getAccessUsers, updateUserAccess } from '../services/adminAccessService';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
|
||||
const areaColumns = [
|
||||
{ key: 'name', label: 'Area' },
|
||||
@ -44,6 +45,7 @@ function mapMockUsers() {
|
||||
|
||||
export function AdminPage() {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [users, setUsers] = useState(mapMockUsers);
|
||||
const [profiles, setProfiles] = useState([]);
|
||||
const [areas, setAreas] = useState([]);
|
||||
@ -207,8 +209,8 @@ export function AdminPage() {
|
||||
title="Painel administrativo"
|
||||
subtitle="Controle de usuarios, perfis, areas e base de conteudo para IA."
|
||||
activeSection="admin"
|
||||
profileLabel="Lucas Admin"
|
||||
initials="LA"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
isDesktop={isDesktop}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
|
||||
@ -5,6 +5,7 @@ import { ManagementTable } from '../components/ManagementTable';
|
||||
import { MetricGrid } from '../components/MetricGrid';
|
||||
import { areaRows, queueRows, supervisorMetrics } from '../services/managementMocks';
|
||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||
|
||||
const queueColumns = [
|
||||
{ key: 'customer', label: 'Cliente' },
|
||||
@ -41,6 +42,7 @@ const areaColumns = [
|
||||
|
||||
export function SupervisorPage() {
|
||||
const { isDesktop, isMobile } = useViewport();
|
||||
const userDisplay = getCurrentUserDisplay();
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
@ -103,8 +105,8 @@ export function SupervisorPage() {
|
||||
title="Painel do supervisor"
|
||||
subtitle="Acompanhamento operacional das filas, areas e distribuicao de atendimento."
|
||||
activeSection="supervisor"
|
||||
profileLabel="Marina Alves"
|
||||
initials="MA"
|
||||
profileLabel={userDisplay.name}
|
||||
initials={userDisplay.initials}
|
||||
isDesktop={isDesktop}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
|
||||
@ -11,6 +11,10 @@ export const WhatsappAdminPage = () => {
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to WhatsApp WebSocket');
|
||||
fetch('http://localhost:3001/whatsapp/status')
|
||||
.then((response) => response.json())
|
||||
.then((data) => setStatus(data.status))
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
socket.on('qr', (qrDataUrl) => {
|
||||
|
||||
@ -1,23 +1,10 @@
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||
import { LoginPage } from '../modules/auth/pages/LoginPage';
|
||||
import { HomePage } from '../modules/home/pages/HomePage';
|
||||
import { ProfileHomePage } from '../modules/home/pages/ProfileHomePage';
|
||||
import { ChatPage } from '../modules/chat/pages/ChatPage';
|
||||
import { CallPage } from '../modules/call/pages/CallPage';
|
||||
import { NewAttendancePage } from '../modules/attendance/pages/NewAttendancePage';
|
||||
import { AdminPage } from '../modules/management/pages/AdminPage';
|
||||
import { SupervisorPage } from '../modules/management/pages/SupervisorPage';
|
||||
import { getCurrentUserProfile } from '../modules/auth/services/sessionService';
|
||||
|
||||
function HomeRouter() {
|
||||
const profile = getCurrentUserProfile();
|
||||
if (profile === 'admin') {
|
||||
return <AdminPage />;
|
||||
}
|
||||
if (profile === 'supervisor') {
|
||||
return <SupervisorPage />;
|
||||
}
|
||||
return <HomePage />;
|
||||
}
|
||||
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
@ -30,7 +17,7 @@ export const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
element: <HomeRouter />,
|
||||
element: <ProfileHomePage />,
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
@ -44,4 +31,8 @@ export const router = createBrowserRouter([
|
||||
path: '/new-attendance',
|
||||
element: <NewAttendancePage />,
|
||||
},
|
||||
{
|
||||
path: '/admin/whatsapp',
|
||||
element: <WhatsappAdminPage />,
|
||||
},
|
||||
]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user