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
|
# Dependencies
|
||||||
dist
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
.env.development
|
.env.development
|
||||||
|
.env.development.local
|
||||||
.env.production
|
.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() {
|
export function LoginForm() {
|
||||||
const [formData, setFormData] = useState(initialFormData);
|
const [formData, setFormData] = useState(initialFormData);
|
||||||
const { login, isSubmitting } = useLogin();
|
const { login, startMicrosoftLogin, providers, error, isSubmitting } = useLogin();
|
||||||
|
|
||||||
async function handleSubmit(event) {
|
async function handleSubmit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -48,50 +48,89 @@ export function LoginForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: '1rem' }}>
|
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: '1rem' }}>
|
||||||
<label style={{ display: 'grid', gap: '0.5rem' }}>
|
{providers.ldap ? (
|
||||||
<span style={{ fontWeight: 600 }}>Usuário AD</span>
|
<>
|
||||||
<input
|
<label style={{ display: 'grid', gap: '0.5rem' }}>
|
||||||
style={fieldStyle}
|
<span style={{ fontWeight: 600 }}>Usuario AD</span>
|
||||||
type="text"
|
<input
|
||||||
placeholder="seu.usuario"
|
style={fieldStyle}
|
||||||
value={formData.username}
|
type="text"
|
||||||
onChange={(event) =>
|
placeholder="seu.usuario"
|
||||||
setFormData((current) => ({ ...current, username: event.target.value }))
|
autoComplete="username"
|
||||||
}
|
value={formData.username}
|
||||||
/>
|
onChange={(event) =>
|
||||||
</label>
|
setFormData((current) => ({ ...current, username: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label style={{ display: 'grid', gap: '0.5rem' }}>
|
<label style={{ display: 'grid', gap: '0.5rem' }}>
|
||||||
<span style={{ fontWeight: 600 }}>Senha</span>
|
<span style={{ fontWeight: 600 }}>Senha</span>
|
||||||
<input
|
<input
|
||||||
style={fieldStyle}
|
style={fieldStyle}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Digite sua senha"
|
placeholder="Digite sua senha"
|
||||||
value={formData.password}
|
autoComplete="current-password"
|
||||||
onChange={(event) =>
|
value={formData.password}
|
||||||
setFormData((current) => ({ ...current, password: event.target.value }))
|
onChange={(event) =>
|
||||||
}
|
setFormData((current) => ({ ...current, password: event.target.value }))
|
||||||
/>
|
}
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<button style={primaryButtonStyle} type="submit" disabled={isSubmitting}>
|
<button style={primaryButtonStyle} type="submit" disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Entrando...' : 'Entrar'}
|
{isSubmitting ? 'Entrando...' : 'Entrar com AD'}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<button style={secondaryButtonStyle} type="button">
|
{providers.microsoft ? (
|
||||||
Entrar com Microsoft
|
<button style={secondaryButtonStyle} type="button" onClick={startMicrosoftLogin}>
|
||||||
</button>
|
Entrar com Microsoft
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<a
|
{error ? (
|
||||||
href="#forgot-password"
|
<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={{
|
style={{
|
||||||
justifySelf: 'center',
|
justifySelf: 'center',
|
||||||
color: 'var(--color-secondary)',
|
color: 'var(--color-text-soft)',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Esqueci minha senha
|
Acesso somente via AD ou Microsoft corporativo.
|
||||||
</a>
|
</span>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,51 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { mockLogin } from '../services/authService';
|
import {
|
||||||
|
getAuthConfig,
|
||||||
|
loginWithAd,
|
||||||
|
startMicrosoftLogin,
|
||||||
|
storeAuthSession,
|
||||||
|
} from '../services/authService';
|
||||||
|
|
||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [providers, setProviders] = useState({ ldap: true, microsoft: false });
|
||||||
|
|
||||||
async function login() {
|
useEffect(() => {
|
||||||
setIsSubmitting(true);
|
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 {
|
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');
|
navigate('/home');
|
||||||
|
} catch (loginError) {
|
||||||
|
setError(loginError.message || 'Falha ao autenticar.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@ -19,6 +53,9 @@ export function useLogin() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
error,
|
||||||
|
providers,
|
||||||
login,
|
login,
|
||||||
|
startMicrosoftLogin,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export function LoginPage() {
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
MVP de atendimento
|
Atendimento Múltiplos canais
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<h1
|
||||||
style={{
|
style={{
|
||||||
@ -67,7 +67,7 @@ export function LoginPage() {
|
|||||||
lineHeight: 1.05,
|
lineHeight: 1.05,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Conecte-se com seu cliente em uma única tela.
|
Conexão multiatendimento em um único lugar.
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
style={{
|
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: 'Fila', value: 'Distribuição rápida' },
|
||||||
{ label: 'UX', value: 'Padrão SaaS responsivo' },
|
{ label: 'UX', value: 'Padrão SaaS responsivo' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
@ -147,8 +147,7 @@ export function LoginPage() {
|
|||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Use seu usuário corporativo para acessar o MVP. A autenticação e mockada
|
Use seu usuario corporativo para acessar o MVP com Active Directory ou Microsoft.
|
||||||
nesta etapa e leva você diretamente para a dashboard principal.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
async function parseJsonResponse(response) {
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, networkDelay));
|
const data = await response.json().catch(() => null);
|
||||||
|
|
||||||
return {
|
if (!response.ok) {
|
||||||
id: 'agent-001',
|
throw new Error(data?.message || 'Nao foi possivel autenticar.');
|
||||||
name: 'Ana Camolesi',
|
}
|
||||||
email: 'ana.camolesi@sothis.local',
|
|
||||||
};
|
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 { 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 }) {
|
export function HomeSidebar({ items, activeItem, isMobile = false }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -29,7 +29,7 @@ export function HomeSidebar({ items, activeItem, isMobile = false }) {
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+ Novo Atendimento
|
Abrir atendimento
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<nav
|
<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({
|
export function HomeTopbar({
|
||||||
activeTab,
|
activeTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
@ -8,19 +21,29 @@ export function HomeTopbar({
|
|||||||
isTablet = false,
|
isTablet = false,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
}) {
|
}) {
|
||||||
|
const userDisplay = getCurrentUserDisplay();
|
||||||
|
const [currentDateTime, setCurrentDateTime] = useState(() => formatCurrentDateTime(new Date()));
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'messages', label: 'Mensagens' },
|
{ id: 'messages', label: 'Mensagens' },
|
||||||
{ id: 'calls', label: 'Ligações' },
|
{ id: 'calls', label: 'Ligacoes' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const gridTemplateColumns = isMobile
|
const gridTemplateColumns = isMobile
|
||||||
? '1fr'
|
? '1fr'
|
||||||
: isWideDesktop
|
: isWideDesktop
|
||||||
? 'max-content minmax(180px, 220px) minmax(280px, 1fr) max-content'
|
? 'max-content minmax(150px, 190px) minmax(280px, 1fr) max-content'
|
||||||
: isDesktop || isTablet
|
: isDesktop || isTablet
|
||||||
? 'repeat(2, minmax(0, 1fr))'
|
? 'repeat(2, minmax(0, 1fr))'
|
||||||
: '1fr';
|
: '1fr';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = window.setInterval(() => {
|
||||||
|
setCurrentDateTime(formatCurrentDateTime(new Date()));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => window.clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
style={{
|
style={{
|
||||||
@ -75,9 +98,13 @@ export function HomeTopbar({
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
width: isMobile ? '100%' : 'auto',
|
width: isMobile ? '100%' : 'auto',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sexta, 19 de março
|
{currentDateTime}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -108,9 +135,9 @@ export function HomeTopbar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ textAlign: 'right', minWidth: 0 }}>
|
<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' }}>
|
<span style={{ color: 'var(--color-text-soft)', fontSize: '0.92rem' }}>
|
||||||
Atendimento omnichannel
|
{userDisplay.subtitle}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -126,7 +153,7 @@ export function HomeTopbar({
|
|||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
AM
|
{userDisplay.initials}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const WORKSPACE_HEIGHT = 660;
|
||||||
|
|
||||||
function ChannelBadge({ channel }) {
|
function ChannelBadge({ channel }) {
|
||||||
const colors = {
|
const colors = {
|
||||||
WhatsApp: '#2bb741',
|
WhatsApp: '#2bb741',
|
||||||
@ -25,28 +28,145 @@ 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({
|
export function MessagesWorkspace({
|
||||||
conversations,
|
conversations,
|
||||||
activeConversationId,
|
activeConversationId,
|
||||||
onSelectConversation,
|
onSelectConversation,
|
||||||
actionItems,
|
|
||||||
isWideDesktop = false,
|
isWideDesktop = false,
|
||||||
isDesktop = false,
|
isDesktop = false,
|
||||||
isTablet = false,
|
isTablet = false,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const recentConversations = conversations.slice(0, 3);
|
||||||
const activeConversation =
|
const activeConversation =
|
||||||
conversations.find((conversation) => conversation.id === activeConversationId) ||
|
recentConversations.find((conversation) => conversation.id === activeConversationId) ||
|
||||||
|
recentConversations[0] ||
|
||||||
conversations[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
|
const gridTemplateColumns = isMobile
|
||||||
? '1fr'
|
? '1fr'
|
||||||
: isWideDesktop
|
: isWideDesktop
|
||||||
? 'minmax(240px, 0.95fr) minmax(360px, 1.8fr) minmax(220px, 0.8fr)'
|
? 'minmax(240px, 0.95fr) minmax(360px, 1.8fr) minmax(220px, 0.8fr)'
|
||||||
: isDesktop || isTablet
|
: isDesktop || isTablet
|
||||||
? 'minmax(260px, 320px) minmax(0, 1fr)'
|
? 'minmax(260px, 320px) minmax(0, 1fr)'
|
||||||
: '1fr';
|
: '1fr';
|
||||||
|
|
||||||
|
const panelHeight = isMobile ? 'auto' : WORKSPACE_HEIGHT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -54,7 +174,7 @@ export function MessagesWorkspace({
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns,
|
gridTemplateColumns,
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
alignItems: 'start',
|
alignItems: 'stretch',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
@ -65,18 +185,20 @@ export function MessagesWorkspace({
|
|||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '0.75rem',
|
gap: '0.75rem',
|
||||||
|
alignContent: 'start',
|
||||||
|
height: panelHeight,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong style={{ fontSize: '1.05rem' }}>Conversas</strong>
|
<strong style={{ fontSize: '1.05rem' }}>Conversas</strong>
|
||||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{conversations.map((conversation) => {
|
{recentConversations.map((conversation) => {
|
||||||
const isActive = conversation.id === activeConversation.id;
|
const isActive = conversation.id === safeActiveConversation.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -123,6 +245,23 @@ export function MessagesWorkspace({
|
|||||||
</button>
|
</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>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@ -131,8 +270,9 @@ export function MessagesWorkspace({
|
|||||||
borderRadius: '26px',
|
borderRadius: '26px',
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateRows: 'auto 1fr auto',
|
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
||||||
minHeight: 580,
|
height: panelHeight,
|
||||||
|
minHeight: isMobile ? 580 : 'auto',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
@ -148,9 +288,11 @@ export function MessagesWorkspace({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<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)' }}>
|
<span style={{ color: 'var(--color-text-soft)' }}>
|
||||||
{activeConversation.status === 'online' ? 'Online agora' : 'Offline'}
|
{safeActiveConversation.status === 'online' ? 'Online agora' : 'Offline'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||||
@ -190,11 +332,12 @@ export function MessagesWorkspace({
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
gap: '0.9rem',
|
gap: '0.9rem',
|
||||||
alignContent: 'start',
|
alignContent: 'start',
|
||||||
|
overflowY: 'auto',
|
||||||
background:
|
background:
|
||||||
'linear-gradient(180deg, rgba(245, 248, 251, 0.45), rgba(255, 255, 255, 0.9))',
|
'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';
|
const isAgent = message.from === 'agent';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -218,37 +361,72 @@ export function MessagesWorkspace({
|
|||||||
|
|
||||||
<footer
|
<footer
|
||||||
style={{
|
style={{
|
||||||
padding: '1rem 1.25rem 1.25rem',
|
padding: '0.85rem 1.25rem 1rem',
|
||||||
borderTop: '1px solid var(--color-border)',
|
borderTop: '1px solid var(--color-border)',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '1fr auto',
|
gap: '0.65rem',
|
||||||
gap: '0.75rem',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<strong style={{ display: 'block', fontSize: '0.94rem' }}>Resposta sugerida</strong>
|
||||||
type="text"
|
|
||||||
value="Posso acionar o time responsavel e te retorno em seguida."
|
<div
|
||||||
readOnly
|
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
display: 'grid',
|
||||||
borderRadius: '18px',
|
gridTemplateColumns: '40px minmax(0, 1fr) 40px',
|
||||||
padding: '0.95rem 1rem',
|
gap: '0.6rem',
|
||||||
background: '#fff',
|
alignItems: 'stretch',
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '18px',
|
|
||||||
padding: '0.95rem 1.2rem',
|
|
||||||
background: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
|
||||||
color: '#fff',
|
|
||||||
fontWeight: 700,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Enviar
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
onClick={selectPreviousReply}
|
||||||
|
title="Resposta anterior"
|
||||||
|
style={{
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
borderRadius: '14px',
|
||||||
|
background: '#fff',
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
fontWeight: 900,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</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>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -259,49 +437,102 @@ export function MessagesWorkspace({
|
|||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
padding: '1.2rem',
|
padding: '1.2rem',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
alignContent: 'start',
|
|
||||||
gridColumn: isWideDesktop ? 'auto' : '1 / -1',
|
gridColumn: isWideDesktop ? 'auto' : '1 / -1',
|
||||||
|
height: panelHeight,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong style={{ fontSize: '1.05rem' }}>Painel de ações</strong>
|
<strong style={{ fontSize: '1.05rem' }}>Comunicados e notas</strong>
|
||||||
<p style={{ margin: '0.35rem 0 0', color: 'var(--color-text-soft)' }}>
|
|
||||||
Contexto rápido do atendimento selecionado.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actionItems.map((item) => (
|
<div
|
||||||
<article
|
|
||||||
key={item.title}
|
|
||||||
style={{
|
|
||||||
borderRadius: '20px',
|
|
||||||
padding: '1rem',
|
|
||||||
background: 'rgba(0, 49, 80, 0.04)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ color: 'var(--color-text-soft)', display: 'block', marginBottom: '0.35rem' }}>
|
|
||||||
{item.title}
|
|
||||||
</span>
|
|
||||||
<strong>{item.value}</strong>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate('/new-attendance')}
|
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid var(--color-border)',
|
display: 'grid',
|
||||||
borderRadius: '18px',
|
gap: '0.85rem',
|
||||||
padding: '0.95rem 1rem',
|
alignContent: 'start',
|
||||||
background: '#fff',
|
overflowY: 'auto',
|
||||||
color: 'var(--color-primary)',
|
paddingRight: '0.15rem',
|
||||||
fontWeight: 700,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Criar novo fluxo
|
{managerMessages.map((message) => (
|
||||||
</button>
|
<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>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<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: 'linear-gradient(135deg, var(--color-primary), #0b5a86)',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 800,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
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>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { HomeTopbar } from '../components/HomeTopbar';
|
|||||||
import { MessagesWorkspace } from '../components/MessagesWorkspace';
|
import { MessagesWorkspace } from '../components/MessagesWorkspace';
|
||||||
import { CallsWorkspace } from '../components/CallsWorkspace';
|
import { CallsWorkspace } from '../components/CallsWorkspace';
|
||||||
import { AttendantOpsPanel } from '../components/AttendantOpsPanel';
|
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 { useViewport } from '../../../shared/hooks/useViewport';
|
||||||
import { useChat } from '../../chat/hooks/useChat';
|
import { useChat } from '../../chat/hooks/useChat';
|
||||||
|
|
||||||
@ -141,7 +141,6 @@ export function HomePage() {
|
|||||||
conversations={filteredConversations}
|
conversations={filteredConversations}
|
||||||
activeConversationId={safeConversationId}
|
activeConversationId={safeConversationId}
|
||||||
onSelectConversation={setActiveContactId}
|
onSelectConversation={setActiveContactId}
|
||||||
actionItems={actionItems}
|
|
||||||
isWideDesktop={isWideDesktop}
|
isWideDesktop={isWideDesktop}
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
export const sidebarItems = [
|
export const sidebarItems = [
|
||||||
{ id: 'dashboard', label: 'Dashboard' },
|
{ id: 'scripts', label: 'Scripts e respostas prontas' },
|
||||||
{ id: 'new-attendance', label: 'Novos Atendimentos', route: '/new-attendance' },
|
{ id: 'personal-reports', label: 'Relatorios pessoais' },
|
||||||
{ id: 'in-progress', label: 'Em andamento', count: 8 },
|
{ id: 'mass-message', label: 'Disparo em massa' },
|
||||||
{ id: 'completed', label: 'Finalizados', count: 24 },
|
{ id: 'knowledge-base', label: 'Base de conhecimento' },
|
||||||
{ id: 'contacts', label: 'Contatos', count: 128 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const conversations = [
|
export const conversations = [
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export function ManagementLayout({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(activeSection === 'admin' ? '/admin' : '/supervisor')}
|
onClick={() => navigate('/home')}
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '20px',
|
borderRadius: '20px',
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { MetricGrid } from '../components/MetricGrid';
|
|||||||
import { adminMetrics, aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
import { adminMetrics, aiContentRows, areaRows, userRows } from '../services/managementMocks';
|
||||||
import { getAccessOptions, getAccessUsers, updateUserAccess } from '../services/adminAccessService';
|
import { getAccessOptions, getAccessUsers, updateUserAccess } from '../services/adminAccessService';
|
||||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||||
|
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||||
|
|
||||||
const areaColumns = [
|
const areaColumns = [
|
||||||
{ key: 'name', label: 'Area' },
|
{ key: 'name', label: 'Area' },
|
||||||
@ -44,6 +45,7 @@ function mapMockUsers() {
|
|||||||
|
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
const { isDesktop, isMobile } = useViewport();
|
const { isDesktop, isMobile } = useViewport();
|
||||||
|
const userDisplay = getCurrentUserDisplay();
|
||||||
const [users, setUsers] = useState(mapMockUsers);
|
const [users, setUsers] = useState(mapMockUsers);
|
||||||
const [profiles, setProfiles] = useState([]);
|
const [profiles, setProfiles] = useState([]);
|
||||||
const [areas, setAreas] = useState([]);
|
const [areas, setAreas] = useState([]);
|
||||||
@ -207,8 +209,8 @@ export function AdminPage() {
|
|||||||
title="Painel administrativo"
|
title="Painel administrativo"
|
||||||
subtitle="Controle de usuarios, perfis, areas e base de conteudo para IA."
|
subtitle="Controle de usuarios, perfis, areas e base de conteudo para IA."
|
||||||
activeSection="admin"
|
activeSection="admin"
|
||||||
profileLabel="Lucas Admin"
|
profileLabel={userDisplay.name}
|
||||||
initials="LA"
|
initials={userDisplay.initials}
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { ManagementTable } from '../components/ManagementTable';
|
|||||||
import { MetricGrid } from '../components/MetricGrid';
|
import { MetricGrid } from '../components/MetricGrid';
|
||||||
import { areaRows, queueRows, supervisorMetrics } from '../services/managementMocks';
|
import { areaRows, queueRows, supervisorMetrics } from '../services/managementMocks';
|
||||||
import { useViewport } from '../../../shared/hooks/useViewport';
|
import { useViewport } from '../../../shared/hooks/useViewport';
|
||||||
|
import { getCurrentUserDisplay } from '../../auth/services/sessionService';
|
||||||
|
|
||||||
const queueColumns = [
|
const queueColumns = [
|
||||||
{ key: 'customer', label: 'Cliente' },
|
{ key: 'customer', label: 'Cliente' },
|
||||||
@ -41,6 +42,7 @@ const areaColumns = [
|
|||||||
|
|
||||||
export function SupervisorPage() {
|
export function SupervisorPage() {
|
||||||
const { isDesktop, isMobile } = useViewport();
|
const { isDesktop, isMobile } = useViewport();
|
||||||
|
const userDisplay = getCurrentUserDisplay();
|
||||||
const [templates, setTemplates] = useState([]);
|
const [templates, setTemplates] = useState([]);
|
||||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||||
const [editName, setEditName] = useState('');
|
const [editName, setEditName] = useState('');
|
||||||
@ -103,8 +105,8 @@ export function SupervisorPage() {
|
|||||||
title="Painel do supervisor"
|
title="Painel do supervisor"
|
||||||
subtitle="Acompanhamento operacional das filas, areas e distribuicao de atendimento."
|
subtitle="Acompanhamento operacional das filas, areas e distribuicao de atendimento."
|
||||||
activeSection="supervisor"
|
activeSection="supervisor"
|
||||||
profileLabel="Marina Alves"
|
profileLabel={userDisplay.name}
|
||||||
initials="MA"
|
initials={userDisplay.initials}
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,6 +11,10 @@ export const WhatsappAdminPage = () => {
|
|||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
console.log('Connected to WhatsApp WebSocket');
|
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) => {
|
socket.on('qr', (qrDataUrl) => {
|
||||||
|
|||||||
@ -1,23 +1,10 @@
|
|||||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||||
import { LoginPage } from '../modules/auth/pages/LoginPage';
|
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 { ChatPage } from '../modules/chat/pages/ChatPage';
|
||||||
import { CallPage } from '../modules/call/pages/CallPage';
|
import { CallPage } from '../modules/call/pages/CallPage';
|
||||||
import { NewAttendancePage } from '../modules/attendance/pages/NewAttendancePage';
|
import { NewAttendancePage } from '../modules/attendance/pages/NewAttendancePage';
|
||||||
import { AdminPage } from '../modules/management/pages/AdminPage';
|
import { WhatsappAdminPage } from '../modules/management/pages/WhatsappAdminPage';
|
||||||
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 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -30,7 +17,7 @@ export const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/home',
|
path: '/home',
|
||||||
element: <HomeRouter />,
|
element: <ProfileHomePage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
@ -44,4 +31,8 @@ export const router = createBrowserRouter([
|
|||||||
path: '/new-attendance',
|
path: '/new-attendance',
|
||||||
element: <NewAttendancePage />,
|
element: <NewAttendancePage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/whatsapp',
|
||||||
|
element: <WhatsappAdminPage />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user