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:
Rafael Alves Lopes 2026-05-18 19:11:01 -03:00
parent de1e4f518b
commit 2229a29af1
21 changed files with 542 additions and 244 deletions

31
.gitignore vendored
View File

@ -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/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

View File

@ -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}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

14
dist/index.html vendored
View File

@ -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>

View File

@ -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>
);
}

View File

@ -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,
};
}

View File

@ -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>

View File

@ -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));
}

View File

@ -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

View File

@ -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>

View File

@ -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>
);

View File

@ -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}

View File

@ -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 = [

View File

@ -91,7 +91,7 @@ export function ManagementLayout({
>
<button
type="button"
onClick={() => navigate(activeSection === 'admin' ? '/admin' : '/supervisor')}
onClick={() => navigate('/home')}
style={{
border: 'none',
borderRadius: '20px',

View File

@ -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}
>

View File

@ -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}
>

View File

@ -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) => {

View File

@ -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 />,
},
]);