Primeiro commit

This commit is contained in:
2025-10-14 14:04:17 -03:00
commit 33d8645eb4
109 changed files with 55424 additions and 0 deletions

20879
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
client/package.json Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "liberi-kids-client",
"version": "1.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"axios": "^1.5.0",
"react-icons": "^4.11.0",
"recharts": "^2.8.0",
"react-hook-form": "^7.46.1",
"react-hot-toast": "^2.4.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:5000"
}

2693
client/src/App.css Normal file

File diff suppressed because it is too large Load Diff

51
client/src/App.js Normal file
View File

@@ -0,0 +1,51 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import Layout from './components/Layout';
import { NotificationProvider } from './components/NotificationCenter';
import Dashboard from './pages/DashboardSimples';
import Produtos from './pages/Produtos';
import Clientes from './pages/Clientes';
import Fornecedores from './pages/Fornecedores';
import Despesas from './pages/Despesas';
import Vendas from './pages/Vendas';
import Devolucoes from './pages/Devolucoes';
import Emprestimos from './pages/Emprestimos';
import Configuracoes from './pages/Configuracoes';
import './App.css';
function App() {
return (
<Router>
<NotificationProvider>
<div className="App">
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
}}
/>
<Layout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/produtos" element={<Produtos />} />
<Route path="/clientes" element={<Clientes />} />
<Route path="/fornecedores" element={<Fornecedores />} />
<Route path="/despesas" element={<Despesas />} />
<Route path="/vendas" element={<Vendas />} />
<Route path="/devolucoes" element={<Devolucoes />} />
<Route path="/emprestimos" element={<Emprestimos />} />
<Route path="/configuracoes" element={<Configuracoes />} />
</Routes>
</Layout>
</div>
</NotificationProvider>
</Router>
);
}
export default App;

View File

@@ -0,0 +1,343 @@
/* Chat WhatsApp Overlay */
.chat-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.chat-container {
background: white;
border-radius: 12px;
width: 100%;
max-width: 480px;
height: 600px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* Header */
.chat-header {
background: #075e54;
color: white;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-client-info {
display: flex;
align-items: center;
gap: 12px;
}
.chat-avatar {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.chat-client-details h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.chat-phone {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
opacity: 0.8;
margin-top: 2px;
}
.chat-close-btn {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 8px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.chat-close-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
/* Messages Area */
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
background: #e5ddd5;
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23d4d4d4' fill-opacity='0.1'%3E%3Cpath d='M20 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20z'/%3E%3C/g%3E%3C/svg%3E");
}
.chat-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #e0e0e0;
border-top: 3px solid #25d366;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.chat-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #666;
text-align: center;
}
.chat-empty svg {
margin-bottom: 16px;
opacity: 0.5;
}
.chat-empty p {
margin: 0 0 8px 0;
font-weight: 500;
}
.chat-empty span {
font-size: 14px;
opacity: 0.7;
}
/* Date Divider */
.chat-date-divider {
text-align: center;
margin: 16px 0;
position: relative;
}
.chat-date-divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: rgba(0, 0, 0, 0.1);
}
.chat-date-divider::after {
content: attr(data-date);
background: rgba(255, 255, 255, 0.9);
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
color: #666;
position: relative;
z-index: 1;
}
/* Messages */
.chat-message {
margin-bottom: 8px;
display: flex;
}
.chat-message.enviada {
justify-content: flex-end;
}
.chat-message.recebida {
justify-content: flex-start;
}
.message-content {
max-width: 70%;
padding: 8px 12px;
border-radius: 8px;
position: relative;
}
.chat-message.enviada .message-content {
background: #dcf8c6;
border-bottom-right-radius: 2px;
}
.chat-message.recebida .message-content {
background: white;
border-bottom-left-radius: 2px;
}
.message-content p {
margin: 0 0 4px 0;
word-wrap: break-word;
line-height: 1.4;
}
.message-info {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
margin-top: 4px;
}
.message-time {
font-size: 11px;
color: #666;
}
.message-status {
font-size: 12px;
line-height: 1;
}
.message-status.enviando {
color: #999;
}
.message-status.enviada {
color: #4fc3f7;
}
.message-status.entregue {
color: #4fc3f7;
}
.message-status.lida {
color: #25d366;
}
/* Input Area */
.chat-input-area {
background: white;
padding: 16px;
border-top: 1px solid #e0e0e0;
}
.chat-input-container {
display: flex;
align-items: flex-end;
gap: 8px;
background: #f5f5f5;
border-radius: 24px;
padding: 8px 12px;
}
.chat-input {
flex: 1;
border: none;
background: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
line-height: 1.4;
max-height: 100px;
min-height: 20px;
}
.chat-input::placeholder {
color: #999;
}
.chat-send-btn {
background: #25d366;
border: none;
color: white;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.chat-send-btn:hover:not(:disabled) {
background: #128c7e;
transform: scale(1.05);
}
.chat-send-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
/* Responsive */
@media (max-width: 768px) {
.chat-overlay {
padding: 0;
}
.chat-container {
height: 100vh;
max-width: none;
border-radius: 0;
}
.chat-header {
padding: 12px 16px;
}
.chat-messages {
padding: 12px;
}
.chat-input-area {
padding: 12px;
}
}
/* Scrollbar personalizada */
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: transparent;
}
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,218 @@
import React, { useState, useEffect, useRef } from 'react';
import { FiX, FiSend, FiPhone, FiMessageCircle } from 'react-icons/fi';
import './ChatWhatsApp.css';
const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
const [mensagens, setMensagens] = useState([]);
const [novaMensagem, setNovaMensagem] = useState('');
const [enviando, setEnviando] = useState(false);
const [carregando, setCarregando] = useState(false);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [mensagens]);
useEffect(() => {
if (isOpen && cliente?.telefone) {
carregarHistorico();
}
}, [isOpen, cliente]);
const carregarHistorico = async () => {
setCarregando(true);
try {
const response = await fetch(`/api/chat/${cliente.telefone}`);
const data = await response.json();
if (response.ok) {
setMensagens(data.data || []);
} else {
console.error('Erro ao carregar histórico:', data.error);
}
} catch (error) {
console.error('Erro ao carregar histórico:', error);
} finally {
setCarregando(false);
}
};
const enviarMensagem = async () => {
if (!novaMensagem.trim() || enviando) return;
setEnviando(true);
const mensagemTemp = {
id: Date.now(),
mensagem: novaMensagem,
tipo: 'enviada',
created_at: new Date().toISOString(),
status: 'enviando'
};
// Adicionar mensagem temporária
setMensagens(prev => [...prev, mensagemTemp]);
setNovaMensagem('');
try {
const response = await fetch('/api/chat/enviar', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
telefone: cliente.telefone,
mensagem: novaMensagem,
clienteNome: cliente.nome
})
});
const data = await response.json();
if (response.ok) {
// Atualizar mensagem temporária com dados reais
setMensagens(prev =>
prev.map(msg =>
msg.id === mensagemTemp.id
? { ...data.data, status: 'enviada' }
: msg
)
);
} else {
// Remover mensagem temporária e mostrar erro
setMensagens(prev => prev.filter(msg => msg.id !== mensagemTemp.id));
alert(`Erro ao enviar mensagem: ${data.error}`);
}
} catch (error) {
console.error('Erro ao enviar mensagem:', error);
setMensagens(prev => prev.filter(msg => msg.id !== mensagemTemp.id));
alert('Erro ao enviar mensagem. Tente novamente.');
} finally {
setEnviando(false);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
enviarMensagem();
}
};
const formatarHora = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit'
});
};
const formatarData = (timestamp) => {
return new Date(timestamp).toLocaleDateString('pt-BR');
};
if (!isOpen) return null;
return (
<div className="chat-overlay">
<div className="chat-container">
{/* Header */}
<div className="chat-header">
<div className="chat-client-info">
<div className="chat-avatar">
<FiMessageCircle size={24} />
</div>
<div className="chat-client-details">
<h3>{cliente?.nome || 'Cliente'}</h3>
<span className="chat-phone">
<FiPhone size={14} />
{cliente?.telefone}
</span>
</div>
</div>
<button className="chat-close-btn" onClick={onClose}>
<FiX size={20} />
</button>
</div>
{/* Messages Area */}
<div className="chat-messages">
{carregando ? (
<div className="chat-loading">
<div className="loading-spinner"></div>
<p>Carregando histórico...</p>
</div>
) : mensagens.length === 0 ? (
<div className="chat-empty">
<FiMessageCircle size={48} />
<p>Nenhuma mensagem ainda</p>
<span>Inicie uma conversa com {cliente?.nome}</span>
</div>
) : (
<>
{mensagens.map((mensagem, index) => {
const showDate = index === 0 ||
formatarData(mensagem.created_at) !== formatarData(mensagens[index - 1].created_at);
return (
<React.Fragment key={mensagem.id || index}>
{showDate && (
<div className="chat-date-divider">
{formatarData(mensagem.created_at)}
</div>
)}
<div className={`chat-message ${mensagem.tipo}`}>
<div className="message-content">
<p>{mensagem.mensagem}</p>
<div className="message-info">
<span className="message-time">
{formatarHora(mensagem.created_at)}
</span>
{mensagem.tipo === 'enviada' && (
<span className={`message-status ${mensagem.status}`}>
{mensagem.status === 'enviando' ? '⏳' :
mensagem.status === 'enviada' ? '✓' :
mensagem.status === 'entregue' ? '✓✓' :
mensagem.status === 'lida' ? '✓✓' : ''}
</span>
)}
</div>
</div>
</div>
</React.Fragment>
);
})}
<div ref={messagesEndRef} />
</>
)}
</div>
{/* Input Area */}
<div className="chat-input-area">
<div className="chat-input-container">
<textarea
value={novaMensagem}
onChange={(e) => setNovaMensagem(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Digite sua mensagem..."
className="chat-input"
rows="1"
disabled={enviando}
/>
<button
onClick={enviarMensagem}
disabled={!novaMensagem.trim() || enviando}
className="chat-send-btn"
>
<FiSend size={18} />
</button>
</div>
</div>
</div>
</div>
);
};
export default ChatWhatsApp;

View File

@@ -0,0 +1,325 @@
.layout {
display: flex;
min-height: 100vh;
background-color: #f8fafc;
}
/* Sidebar */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #1e293b 0%, #334155 100%);
color: white;
position: fixed;
top: 0;
left: 0;
height: 100vh;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
overflow-y: auto;
}
.sidebar-open {
transform: translateX(0);
}
.sidebar-header {
padding: 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-img {
width: 80px;
height: 80px;
border-radius: 12px;
object-fit: cover;
}
.logo-text h2 {
font-size: 18px;
font-weight: 700;
margin: 0;
color: white;
}
.logo-text span {
font-size: 12px;
color: #cbd5e1;
font-weight: 400;
}
.sidebar-close {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s ease;
display: none;
}
.sidebar-close:hover {
background: rgba(255, 255, 255, 0.1);
}
.sidebar-nav {
padding: 16px 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
color: #cbd5e1;
text-decoration: none;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.05);
color: white;
}
.nav-item-active {
background: rgba(255, 255, 255, 0.1);
color: white;
border-left-color: #667eea;
}
.nav-icon {
font-size: 18px;
flex-shrink: 0;
}
.nav-label {
font-weight: 500;
font-size: 14px;
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: none;
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.header {
background: white;
padding: 16px 24px;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.menu-toggle {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 8px;
border-radius: 6px;
color: #374151;
transition: background-color 0.2s ease;
}
.menu-toggle:hover {
background: #f3f4f6;
}
.header-title h1 {
font-size: 20px;
font-weight: 600;
color: #111827;
margin: 0;
}
.header-title p {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.header-actions {
margin-left: auto;
}
.page-content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
/* Desktop */
@media (min-width: 1024px) {
.sidebar {
position: static;
transform: translateX(0);
width: 280px;
}
.main-content {
margin-left: 0;
}
.menu-toggle {
display: none;
}
.sidebar-close {
display: none;
}
}
/* Tablet */
@media (max-width: 1023px) and (min-width: 768px) {
.sidebar-overlay {
display: block;
}
.sidebar-close {
display: block;
}
}
/* Mobile */
@media (max-width: 767px) {
.sidebar {
width: 100%;
max-width: 320px;
}
.sidebar-overlay {
display: block;
}
.sidebar-close {
display: block;
}
.header {
padding: 12px 16px;
}
.header-title h1 {
font-size: 18px;
}
.header-title p {
font-size: 12px;
}
.page-content {
padding: 16px;
}
.logo-text h2 {
font-size: 16px;
}
.nav-item {
padding: 16px 24px;
}
.nav-label {
font-size: 16px;
}
.connection-text {
display: none;
}
.connection-status {
padding: 6px 8px;
}
}
/* =====================================================
STATUS DE CONEXÃO
===================================================== */
.connection-status {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
transition: all 0.3s ease;
}
.connection-icon {
width: 14px;
height: 14px;
}
.connection-text {
font-size: 12px;
}
.connection-connected {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.connection-disconnected {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.2);
}
.connection-error {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
border: 1px solid rgba(245, 158, 11, 0.2);
}
/* Animação de pulso para status offline */
.connection-disconnected,
.connection-error {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}

View File

@@ -0,0 +1,126 @@
import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
FiHome,
FiPackage,
FiUsers,
FiTruck,
FiDollarSign,
FiShoppingCart,
FiRotateCcw,
FiSettings,
FiMenu,
FiX,
FiWifi,
FiWifiOff,
FiCreditCard
} from 'react-icons/fi';
import './Layout.css';
const Layout = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const location = useLocation();
const menuItems = [
{ path: '/', icon: FiHome, label: 'Dashboard' },
{ path: '/produtos', icon: FiPackage, label: 'Produtos' },
{ path: '/clientes', icon: FiUsers, label: 'Clientes' },
{ path: '/fornecedores', icon: FiTruck, label: 'Fornecedores' },
{ path: '/despesas', icon: FiDollarSign, label: 'Despesas' },
{ path: '/vendas', icon: FiShoppingCart, label: 'Vendas' },
{ path: '/devolucoes', icon: FiRotateCcw, label: 'Devolução/Troca' },
{ path: '/emprestimos', icon: FiCreditCard, label: 'Empréstimos' },
{ path: '/configuracoes', icon: FiSettings, label: 'Configurações' },
];
const toggleSidebar = () => {
setSidebarOpen(!sidebarOpen);
};
return (
<div className="layout">
{/* Sidebar */}
<aside className={`sidebar ${sidebarOpen ? 'sidebar-open' : ''}`}>
<div className="sidebar-header">
<div className="logo">
<img
src="/LogoLiberiKids.png"
alt="Liberi Kids"
className="logo-img"
onError={(e) => {
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'block';
}}
/>
<div className="logo-text" style={{ display: 'none' }}>
<h2>Liberi Kids</h2>
<span>Moda Infantil</span>
</div>
</div>
<button
className="sidebar-close"
onClick={toggleSidebar}
>
<FiX />
</button>
</div>
<nav className="sidebar-nav">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={`nav-item ${isActive ? 'nav-item-active' : ''}`}
onClick={() => setSidebarOpen(false)}
>
<Icon className="nav-icon" />
<span className="nav-label">{item.label}</span>
</Link>
);
})}
</nav>
</aside>
{/* Overlay para mobile */}
{sidebarOpen && (
<div
className="sidebar-overlay"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Main Content */}
<div className="main-content">
{/* Header */}
<header className="header">
<button
className="menu-toggle"
onClick={toggleSidebar}
>
<FiMenu />
</button>
<div className="header-title">
<h1>Sistema de Controle de Estoque</h1>
<p>Liberi Kids - Moda Infantil</p>
</div>
<div className="header-actions">
{/* Espaço para futuras ações */}
</div>
</header>
{/* Page Content */}
<main className="page-content">
{children}
</main>
</div>
</div>
);
};
export default Layout;

View File

@@ -0,0 +1,263 @@
/* =====================================================
CENTRO DE NOTIFICAÇÕES
===================================================== */
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
}
.notification {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 320px;
max-width: 400px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
border-left: 4px solid;
pointer-events: all;
cursor: pointer;
transform: translateX(100%);
animation: slideIn 0.3s ease forwards;
transition: all 0.2s ease;
}
.notification:hover {
transform: translateY(-2px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15), 0 6px 15px rgba(0, 0, 0, 0.08);
}
.notification-success {
border-left-color: #10b981;
}
.notification-error {
border-left-color: #ef4444;
}
.notification-warning {
border-left-color: #f59e0b;
}
.notification-info {
border-left-color: #3b82f6;
}
.notification-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-size: 12px;
color: white;
flex-shrink: 0;
margin-top: 2px;
}
.notification-success .notification-icon {
background: #10b981;
}
.notification-error .notification-icon {
background: #ef4444;
}
.notification-warning .notification-icon {
background: #f59e0b;
}
.notification-info .notification-icon {
background: #3b82f6;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-message {
font-size: 14px;
font-weight: 500;
color: #111827;
margin: 0 0 4px 0;
line-height: 1.4;
}
.notification-time {
font-size: 12px;
color: #6b7280;
}
.notification-close {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: #9ca3af;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.notification-close:hover {
background: #f3f4f6;
color: #374151;
}
/* =====================================================
INDICADOR DE CONEXÃO
===================================================== */
.connection-indicator {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: white;
border-radius: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-size: 12px;
font-weight: 500;
z-index: 9998;
transition: all 0.3s ease;
}
.connection-icon {
width: 16px;
height: 16px;
}
.connection-icon.connected {
color: #10b981;
}
.connection-icon.disconnected {
color: #ef4444;
animation: pulse 2s infinite;
}
.connection-icon.error {
color: #f59e0b;
animation: pulse 2s infinite;
}
.connection-connected {
border: 1px solid rgba(16, 185, 129, 0.2);
background: rgba(16, 185, 129, 0.05);
}
.connection-disconnected {
border: 1px solid rgba(239, 68, 68, 0.2);
background: rgba(239, 68, 68, 0.05);
}
.connection-error {
border: 1px solid rgba(245, 158, 11, 0.2);
background: rgba(245, 158, 11, 0.05);
}
.connection-text {
font-size: 12px;
font-weight: 500;
}
.connection-connected .connection-text {
color: #10b981;
}
.connection-disconnected .connection-text {
color: #ef4444;
}
.connection-error .connection-text {
color: #f59e0b;
}
/* =====================================================
ANIMAÇÕES
===================================================== */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* =====================================================
RESPONSIVO
===================================================== */
@media (max-width: 768px) {
.notification-container {
top: 10px;
right: 10px;
left: 10px;
}
.notification {
min-width: auto;
max-width: none;
}
.connection-indicator {
bottom: 10px;
right: 10px;
}
.connection-text {
display: none;
}
.connection-indicator {
padding: 8px 12px;
}
}
@media (max-width: 480px) {
.notification {
padding: 12px;
}
.notification-message {
font-size: 13px;
}
.notification-time {
font-size: 11px;
}
}

View File

@@ -0,0 +1,190 @@
import React, { useState, useEffect, createContext, useContext } from 'react';
import { FiCheck, FiX, FiAlertTriangle, FiInfo, FiWifi, FiWifiOff } from 'react-icons/fi';
import './NotificationCenter.css';
// Context para notificações globais
const NotificationContext = createContext();
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification deve ser usado dentro de NotificationProvider');
}
return context;
};
// Provider de notificações
export const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('connected');
// Adicionar notificação
const addNotification = (message, type = 'info', duration = 5000) => {
const id = Date.now() + Math.random();
const notification = {
id,
message,
type,
timestamp: new Date(),
duration
};
setNotifications(prev => [...prev, notification]);
// Remover automaticamente após o tempo especificado
if (duration > 0) {
setTimeout(() => {
removeNotification(id);
}, duration);
}
return id;
};
// Remover notificação
const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};
// Notificações de conveniência
const success = (message, duration) => addNotification(message, 'success', duration);
const error = (message, duration) => addNotification(message, 'error', duration);
const warning = (message, duration) => addNotification(message, 'warning', duration);
const info = (message, duration) => addNotification(message, 'info', duration);
// Monitorar conexão
useEffect(() => {
const checkConnection = async () => {
try {
const response = await fetch('/api/dashboard', {
method: 'GET',
signal: AbortSignal.timeout(5000)
});
const newStatus = response.ok ? 'connected' : 'error';
// Notificar mudanças de status
if (connectionStatus !== newStatus) {
if (newStatus === 'connected' && connectionStatus !== 'connected') {
success('Conexão restaurada!', 3000);
} else if (newStatus === 'error') {
warning('Problemas de conexão detectados', 4000);
}
}
setConnectionStatus(newStatus);
} catch (error) {
const newStatus = 'disconnected';
if (connectionStatus !== newStatus) {
error('Conexão perdida com o servidor', 5000);
}
setConnectionStatus(newStatus);
}
};
// Verificar imediatamente
checkConnection();
// Verificar a cada 30 segundos
const interval = setInterval(checkConnection, 30000);
return () => clearInterval(interval);
}, [connectionStatus]);
const value = {
notifications,
connectionStatus,
addNotification,
removeNotification,
success,
error,
warning,
info
};
return (
<NotificationContext.Provider value={value}>
{children}
<NotificationCenter />
</NotificationContext.Provider>
);
};
// Componente de notificações
const NotificationCenter = () => {
const { notifications, removeNotification, connectionStatus } = useNotification();
const getIcon = (type) => {
switch (type) {
case 'success': return <FiCheck />;
case 'error': return <FiX />;
case 'warning': return <FiAlertTriangle />;
default: return <FiInfo />;
}
};
const getConnectionIcon = () => {
switch (connectionStatus) {
case 'connected': return <FiWifi className="connection-icon connected" />;
case 'disconnected': return <FiWifiOff className="connection-icon disconnected" />;
case 'error': return <FiWifiOff className="connection-icon error" />;
default: return <FiWifi className="connection-icon" />;
}
};
const getConnectionText = () => {
switch (connectionStatus) {
case 'connected': return 'Online';
case 'disconnected': return 'Offline';
case 'error': return 'Instável';
default: return 'Verificando...';
}
};
return (
<>
{/* Status de conexão fixo */}
<div className={`connection-indicator connection-${connectionStatus}`}>
{getConnectionIcon()}
<span className="connection-text">{getConnectionText()}</span>
</div>
{/* Container de notificações */}
<div className="notification-container">
{notifications.map((notification) => (
<div
key={notification.id}
className={`notification notification-${notification.type}`}
onClick={() => removeNotification(notification.id)}
>
<div className="notification-icon">
{getIcon(notification.type)}
</div>
<div className="notification-content">
<p className="notification-message">{notification.message}</p>
<span className="notification-time">
{notification.timestamp.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit'
})}
</span>
</div>
<button
className="notification-close"
onClick={(e) => {
e.stopPropagation();
removeNotification(notification.id);
}}
>
<FiX />
</button>
</div>
))}
</div>
</>
);
};
export default NotificationCenter;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { FiGrid, FiList } from 'react-icons/fi';
const ViewToggle = ({ viewMode, onViewModeChange }) => {
return (
<div className="view-toggle">
<button
className={`view-toggle-btn ${viewMode === 'cards' ? 'active' : ''}`}
onClick={() => onViewModeChange('cards')}
title="Visualização em Cards"
>
<FiGrid />
</button>
<button
className={`view-toggle-btn ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => onViewModeChange('list')}
title="Visualização em Lista"
>
<FiList />
</button>
</div>
);
};
export default ViewToggle;

134
client/src/index.css Normal file
View File

@@ -0,0 +1,134 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f8fafc;
color: #1e293b;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* Scrollbar personalizada */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Animações */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
/* Utilitários */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-8 { margin-top: 2rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-6 { padding: 1.5rem; }
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.rounded {
border-radius: 0.375rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.shadow {
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.shadow-lg {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

11
client/src/index.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,411 @@
import React, { useState, useEffect } from 'react';
import {
FiPlus,
FiEdit,
FiTrash2,
FiSearch,
FiUsers,
FiMail,
FiPhone,
FiMapPin,
FiMessageSquare
} from 'react-icons/fi';
import { clientesAPI } from '../services/api';
import ViewToggle from '../components/ViewToggle';
import ChatWhatsApp from '../components/ChatWhatsApp';
import toast from 'react-hot-toast';
const Clientes = () => {
const [clientes, setClientes] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [viewMode, setViewMode] = useState('list');
const [showModal, setShowModal] = useState(false);
const [editingClient, setEditingClient] = useState(null);
const [showChatModal, setShowChatModal] = useState(false);
const [selectedCliente, setSelectedCliente] = useState(null);
const [formData, setFormData] = useState({
nome_completo: '',
email: '',
whatsapp: '',
endereco: ''
});
useEffect(() => {
carregarClientes();
}, []);
const carregarClientes = async () => {
try {
setLoading(true);
const response = await clientesAPI.listar();
setClientes(response.data);
} catch (error) {
console.error('Erro ao carregar clientes:', error);
toast.error('Erro ao carregar clientes');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (editingClient) {
await clientesAPI.atualizar(editingClient.id, formData);
toast.success('Cliente atualizado com sucesso!');
} else {
await clientesAPI.criar(formData);
toast.success('Cliente cadastrado com sucesso!');
}
setShowModal(false);
setEditingClient(null);
setFormData({
nome_completo: '',
email: '',
whatsapp: '',
endereco: ''
});
carregarClientes();
} catch (error) {
console.error('Erro ao salvar cliente:', error);
toast.error('Erro ao salvar cliente');
}
};
const handleEdit = (cliente) => {
setEditingClient(cliente);
setFormData({
nome_completo: cliente.nome_completo,
email: cliente.email || '',
whatsapp: cliente.whatsapp || '',
endereco: cliente.endereco || ''
});
setShowModal(true);
};
const handleDelete = async (id) => {
if (window.confirm('Tem certeza que deseja excluir este cliente?')) {
try {
await clientesAPI.deletar(id);
toast.success('Cliente excluído com sucesso!');
carregarClientes();
} catch (error) {
console.error('Erro ao excluir cliente:', error);
toast.error('Erro ao excluir cliente');
}
}
};
const abrirChat = (cliente) => {
if (!cliente.whatsapp) {
toast.error('Cliente não possui WhatsApp cadastrado');
return;
}
const clienteFormatado = {
nome: cliente.nome_completo,
telefone: cliente.whatsapp.replace(/\D/g, ''),
id: cliente.id
};
setSelectedCliente(clienteFormatado);
setShowChatModal(true);
};
const filteredClientes = clientes.filter(cliente =>
cliente.nome_completo.toLowerCase().includes(searchTerm.toLowerCase()) ||
(cliente.email && cliente.email.toLowerCase().includes(searchTerm.toLowerCase())) ||
(cliente.whatsapp && cliente.whatsapp.includes(searchTerm)) ||
(cliente.id_cliente && cliente.id_cliente.toLowerCase().includes(searchTerm.toLowerCase()))
);
if (loading) {
return (
<div className="loading">
<div>Carregando clientes...</div>
</div>
);
}
return (
<div className="clientes fade-in">
<div className="page-header">
<div>
<h1>Clientes</h1>
<p>Gerencie os clientes da loja</p>
</div>
<div className="header-actions">
<ViewToggle
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Novo Cliente
</button>
</div>
</div>
{/* Barra de Pesquisa */}
<div className="search-box">
<FiSearch className="search-icon" />
<input
type="text"
placeholder="Pesquisar clientes..."
className="search-input"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Lista de Clientes */}
{filteredClientes.length === 0 ? (
<div className="empty-state">
<FiUsers size={48} />
<h3>Nenhum cliente encontrado</h3>
<p>Comece adicionando seu primeiro cliente</p>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Adicionar Cliente
</button>
</div>
) : viewMode === 'cards' ? (
<div className="clients-grid">
{filteredClientes.map((cliente) => (
<div key={cliente.id} className="client-card">
<div className="client-header">
<div className="client-avatar">
<FiUsers />
</div>
<div className="client-actions">
{cliente.whatsapp && (
<button
className="btn-icon btn-success"
onClick={() => abrirChat(cliente)}
title="Abrir Chat WhatsApp"
>
<FiMessageSquare />
</button>
)}
<button
className="btn-icon"
onClick={() => handleEdit(cliente)}
title="Editar"
>
<FiEdit />
</button>
<button
className="btn-icon btn-danger"
onClick={() => handleDelete(cliente.id)}
title="Excluir"
>
<FiTrash2 />
</button>
</div>
</div>
<div className="client-info">
<h3 className="client-name">{cliente.nome_completo}</h3>
{cliente.id_cliente && (
<div className="client-detail">
<span className="client-id">#{cliente.id_cliente}</span>
</div>
)}
{cliente.email && (
<div className="client-detail">
<FiMail className="detail-icon" />
<span>{cliente.email}</span>
</div>
)}
{cliente.whatsapp && (
<div className="client-detail">
<FiPhone className="detail-icon" />
<span>{cliente.whatsapp}</span>
</div>
)}
{cliente.endereco && (
<div className="client-detail">
<FiMapPin className="detail-icon" />
<span>{cliente.endereco}</span>
</div>
)}
<div className="client-meta">
<span className="client-date">
Cadastrado em {new Date(cliente.created_at).toLocaleDateString('pt-BR')}
</span>
</div>
</div>
</div>
))}
</div>
) : (
<div className="list-view">
<div className="list-header clientes-list-header">
<span>ID / Nome</span>
<span>Email</span>
<span>WhatsApp</span>
<span>Endereço</span>
<span>Ações</span>
</div>
{filteredClientes.map((cliente) => (
<div key={cliente.id} className="list-item clientes-list-item">
<div>
<div style={{ fontWeight: '600' }}>
{cliente.id_cliente && <span style={{ color: '#3b82f6', fontSize: '12px', fontWeight: '700' }}>#{cliente.id_cliente} - </span>}
{cliente.nome_completo}
</div>
<div style={{ fontSize: '12px', color: '#6b7280' }}>
Cadastrado em {new Date(cliente.created_at).toLocaleDateString('pt-BR')}
</div>
</div>
<span>{cliente.email || '-'}</span>
<span>{cliente.whatsapp || '-'}</span>
<span title={cliente.endereco} style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{cliente.endereco || '-'}
</span>
<div className="list-item-actions">
{cliente.whatsapp && (
<button
className="btn-icon btn-sm btn-success"
onClick={() => abrirChat(cliente)}
title="Abrir Chat WhatsApp"
>
<FiMessageSquare />
</button>
)}
<button
className="btn-icon btn-sm"
onClick={() => handleEdit(cliente)}
title="Editar"
>
<FiEdit />
</button>
<button
className="btn-icon btn-sm btn-danger"
onClick={() => handleDelete(cliente.id)}
title="Excluir"
>
<FiTrash2 />
</button>
</div>
</div>
))}
</div>
)}
{/* Modal de Cliente */}
{showModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">
{editingClient ? 'Editar Cliente' : 'Novo Cliente'}
</h2>
<button
className="modal-close"
onClick={() => {
setShowModal(false);
setEditingClient(null);
setFormData({
nome_completo: '',
email: '',
whatsapp: '',
endereco: ''
});
}}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label">Nome Completo *</label>
<input
type="text"
className="form-input"
value={formData.nome_completo}
onChange={(e) => setFormData({...formData, nome_completo: e.target.value})}
required
/>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">E-mail</label>
<input
type="email"
className="form-input"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
/>
</div>
<div className="form-group">
<label className="form-label">WhatsApp</label>
<input
type="tel"
className="form-input"
value={formData.whatsapp}
onChange={(e) => setFormData({...formData, whatsapp: e.target.value})}
placeholder="(11) 99999-9999"
/>
</div>
</div>
<div className="form-group">
<label className="form-label">Endereço</label>
<textarea
className="form-textarea"
value={formData.endereco}
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
placeholder="Rua, número, bairro, cidade, CEP"
rows="3"
/>
</div>
<div className="modal-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowModal(false);
setEditingClient(null);
}}
>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
{editingClient ? 'Atualizar' : 'Cadastrar'} Cliente
</button>
</div>
</form>
</div>
</div>
)}
{/* Chat WhatsApp */}
<ChatWhatsApp
isOpen={showChatModal}
onClose={() => setShowChatModal(false)}
cliente={selectedCliente}
/>
</div>
);
};
export default Clientes;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { FiSettings } from 'react-icons/fi';
const Configuracoes = () => {
return (
<div className="configuracoes fade-in">
<div className="page-header">
<div>
<h1>Configurações</h1>
<p>Configure as integrações e preferências do sistema</p>
</div>
</div>
<div className="config-section">
<div className="config-header">
<div className="config-title">
<FiSettings className="config-icon" />
<div>
<h2>Evolution API - WhatsApp</h2>
<p>Configure a integração com Evolution API para envio de mensagens WhatsApp</p>
</div>
</div>
</div>
<div className="config-placeholder">
<p>Configuração da Evolution API em desenvolvimento...</p>
</div>
</div>
</div>
);
};
export default Configuracoes;

View File

@@ -0,0 +1,924 @@
import React, { useState, useEffect } from 'react';
import {
FiPackage,
FiUsers,
FiTruck,
FiDollarSign,
FiShoppingCart,
FiTrendingUp,
FiTrendingDown,
FiBox,
FiX,
FiPlus,
FiMessageCircle,
FiCalendar,
FiClock,
FiAlertCircle
} from 'react-icons/fi';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
} from 'recharts';
import { dashboardAPI, clientesAPI, despesasAPI, fornecedoresAPI } from '../services/api';
import toast from 'react-hot-toast';
const Dashboard = () => {
const [dashboardData, setDashboardData] = useState({
contabilidade: {
receitaBruta: 0,
custosProdutos: 0,
totalDespesas: 0,
lucroReal: 0,
margemLucro: 0
},
resumoFinanceiro: {
receitasMes: 0,
despesasMes: 0,
lucroEstimado: 0,
totalVendas: 0
},
emprestimos: {
totalAberto: 0,
totalQuitado: 0,
quantidade: 0
},
vendasPrazo: {
total: 0,
quantidade: 0,
vendas: []
},
parcelasPendentes: {
quantidade: 0,
parcelas: []
},
estatisticas: {
totalProdutos: 0,
totalClientes: 0,
totalFornecedores: 0,
estoqueTotal: 0
}
});
const [loading, setLoading] = useState(true);
// Estados dos modais
const [showProdutoModal, setShowProdutoModal] = useState(false);
const [showClienteModal, setShowClienteModal] = useState(false);
const [showDespesaModal, setShowDespesaModal] = useState(false);
const [showVendaModal, setShowVendaModal] = useState(false);
// Estados dos formulários
const [fornecedores, setFornecedores] = useState([]);
const [tiposDespesas, setTiposDespesas] = useState([]);
const [clientes, setClientes] = useState([]);
useEffect(() => {
carregarDashboard();
carregarVendasPrazo();
}, []);
const carregarDashboard = async () => {
try {
setLoading(true);
const [dashboardRes, fornecedoresRes, tiposRes, clientesRes] = await Promise.all([
dashboardAPI.obterEstatisticas(),
fornecedoresAPI.listar(),
despesasAPI.listarTipos(),
clientesAPI.listar()
]);
setDashboardData(dashboardRes.data);
setFornecedores(fornecedoresRes.data);
setTiposDespesas(tiposRes.data);
setClientes(clientesRes.data);
} catch (error) {
console.error('Erro ao carregar dashboard:', error);
toast.error('Erro ao carregar dados do dashboard');
} finally {
setLoading(false);
}
};
const carregarVendasPrazo = async () => {
try {
setLoadingVendasPrazo(true);
const response = await fetch('/api/vendas/prazo');
if (response.ok) {
const data = await response.json();
setVendasPrazo(data);
}
} catch (error) {
console.error('Erro ao carregar vendas a prazo:', error);
} finally {
setLoadingVendasPrazo(false);
}
};
const enviarWhatsApp = async (venda) => {
try {
const response = await fetch('/api/whatsapp/enviar-cobranca', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
vendaId: venda.id,
clienteId: venda.cliente_id,
telefone: venda.cliente_whatsapp || venda.cliente_telefone
})
});
if (response.ok) {
toast.success('Mensagem enviada com sucesso!');
} else {
const error = await response.json();
toast.error(error.message || 'Erro ao enviar mensagem');
}
} catch (error) {
console.error('Erro ao enviar WhatsApp:', error);
toast.error('Erro ao enviar mensagem');
}
};
const formatarDataVencimento = (data) => {
const dataVencimento = new Date(data);
const hoje = new Date();
const diffTime = dataVencimento - hoje;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return { texto: `${Math.abs(diffDays)} dias em atraso`, classe: 'vencido' };
} else if (diffDays === 0) {
return { texto: 'Vence hoje', classe: 'hoje' };
} else if (diffDays === 1) {
return { texto: 'Vence amanhã', classe: 'amanha' };
} else if (diffDays <= 7) {
return { texto: `${diffDays} dias`, classe: 'proximo' };
} else {
return { texto: `${diffDays} dias`, classe: 'futuro' };
}
};
// Dados mockados para os gráficos (em um cenário real, viriam da API)
const vendasPorMes = [
{ mes: 'Jan', vendas: 12, valor: 2400 },
{ mes: 'Fev', vendas: 19, valor: 3800 },
{ mes: 'Mar', vendas: 15, valor: 3200 },
{ mes: 'Abr', vendas: 25, valor: 5100 },
{ mes: 'Mai', vendas: 22, valor: 4600 },
{ mes: 'Jun', vendas: 30, valor: 6200 },
];
const produtosPorCategoria = [
{ name: 'Verão', value: 45, color: '#667eea' },
{ name: 'Inverno', value: 35, color: '#764ba2' },
{ name: 'Meia Estação', value: 20, color: '#f093fb' },
];
const estatisticas = [
{
title: 'Total de Produtos',
value: stats.totalProdutos?.count || 0,
icon: FiPackage,
color: '#667eea',
bgColor: '#eef2ff',
trend: '+12%',
trendUp: true
},
{
title: 'Clientes Cadastrados',
value: stats.totalClientes?.count || 0,
icon: FiUsers,
color: '#10b981',
bgColor: '#ecfdf5',
trend: '+8%',
trendUp: true
},
{
title: 'Fornecedores',
value: stats.totalFornecedores?.count || 0,
icon: FiTruck,
color: '#f59e0b',
bgColor: '#fffbeb',
trend: '+2%',
trendUp: true
},
{
title: 'Vendas do Mês',
value: stats.vendasMes?.count || 0,
icon: FiShoppingCart,
color: '#ef4444',
bgColor: '#fef2f2',
trend: '-5%',
trendUp: false
},
{
title: 'Estoque Total',
value: stats.estoqueTotal?.total || 0,
icon: FiBox,
color: '#8b5cf6',
bgColor: '#f5f3ff',
trend: '+15%',
trendUp: true
},
{
title: 'Faturamento Mensal',
value: `R$ ${(stats.vendasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`,
icon: FiDollarSign,
color: '#06b6d4',
bgColor: '#ecfeff',
trend: '+18%',
trendUp: true
}
];
if (loading) {
return (
<div className="loading">
<div>Carregando dashboard...</div>
</div>
);
}
return (
<div className="dashboard fade-in">
<div className="dashboard-header">
<h1>Dashboard</h1>
<p>Visão geral do seu negócio</p>
</div>
{/* Resumo Financeiro e Ações Rápidas - MOVIDO PARA O TOPO */}
<div className="quick-summary">
<div className="summary-card">
<h3>Resumo Financeiro</h3>
<div className="summary-items">
<div className="summary-item">
<span className="summary-label">Receita do Mês:</span>
<span className="summary-value positive">
R$ {(stats.vendasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
</div>
<div className="summary-item">
<span className="summary-label">Despesas do Mês:</span>
<span className="summary-value negative">
R$ {(stats.despesasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
</div>
<div className="summary-item">
<span className="summary-label">Lucro Estimado:</span>
<span className="summary-value positive">
R$ {((stats.vendasMes?.total || 0) - (stats.despesasMes?.total || 0)).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
</div>
</div>
</div>
{/* Vendas a Prazo e Recebimentos */}
<div className="summary-card vendas-prazo-card">
<div className="vendas-prazo-header">
<h3>
<FiCalendar />
Vendas a Prazo & Recebimentos
</h3>
<button
className="btn btn-sm btn-secondary"
onClick={carregarVendasPrazo}
disabled={loadingVendasPrazo}
>
{loadingVendasPrazo ? 'Carregando...' : 'Atualizar'}
</button>
</div>
<div className="vendas-prazo-content">
{loadingVendasPrazo ? (
<div className="loading-vendas">Carregando vendas...</div>
) : vendasPrazo.length === 0 ? (
<div className="empty-vendas">
<FiClock />
<p>Nenhuma venda a prazo encontrada</p>
</div>
) : (
<div className="vendas-prazo-list">
{vendasPrazo.slice(0, 5).map((venda) => {
const vencimento = formatarDataVencimento(venda.data_vencimento);
return (
<div key={venda.id} className={`venda-prazo-item ${vencimento.classe}`}>
<div className="venda-info">
<div className="cliente-nome">{venda.cliente_nome}</div>
<div className="venda-detalhes">
<span className="valor">
R$ {parseFloat(venda.valor_parcela).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
<span className="parcela">
{venda.parcela_atual}/{venda.total_parcelas}
</span>
</div>
</div>
<div className="venda-vencimento">
<span className={`vencimento-badge ${vencimento.classe}`}>
{vencimento.texto}
</span>
{(venda.cliente_whatsapp || venda.cliente_telefone) && (
<button
className="btn-whatsapp"
onClick={() => enviarWhatsApp(venda)}
title="Enviar lembrete via WhatsApp"
>
<FiMessageCircle />
</button>
)}
</div>
</div>
);
})}
{vendasPrazo.length > 5 && (
<div className="ver-mais">
<button className="btn btn-link">
Ver todas as {vendasPrazo.length} vendas a prazo
</button>
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* Gráficos */}
<div className="charts-grid">
{/* Gráfico de Vendas por Mês */}
<div className="chart-card">
<div className="chart-header">
<h3>Vendas por Mês</h3>
<p>Últimos 6 meses</p>
</div>
<div className="chart-container">
<ResponsiveContainer width="100%" height={300}>
<BarChart data={vendasPorMes}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="mes"
stroke="#6b7280"
fontSize={12}
/>
<YAxis
stroke="#6b7280"
fontSize={12}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
}}
/>
<Bar
dataKey="vendas"
fill="url(#colorGradient)"
radius={[4, 4, 0, 0]}
/>
<defs>
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#667eea" stopOpacity={0.9}/>
<stop offset="95%" stopColor="#764ba2" stopOpacity={0.9}/>
</linearGradient>
</defs>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Gráfico de Produtos por Categoria */}
<div className="chart-card">
<div className="chart-header">
<h3>Produtos por Estação</h3>
<p>Distribuição do estoque</p>
</div>
<div className="chart-container">
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={produtosPorCategoria}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
dataKey="value"
>
{produtosPorCategoria.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)'
}}
/>
</PieChart>
</ResponsiveContainer>
<div className="pie-legend">
{produtosPorCategoria.map((item, index) => (
<div key={index} className="legend-item">
<div
className="legend-color"
style={{ backgroundColor: item.color }}
></div>
<span>{item.name}</span>
<span className="legend-value">{item.value}%</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* Modais */}
{showClienteModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2>Novo Cliente</h2>
<button
className="btn-icon"
onClick={() => setShowClienteModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-content">
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
await clientesAPI.criar({
nome_completo: formData.get('nome_completo'),
email: formData.get('email'),
telefone: formData.get('telefone'),
whatsapp: formData.get('whatsapp'),
endereco: formData.get('endereco')
});
toast.success('Cliente criado com sucesso!');
setShowClienteModal(false);
carregarDashboard();
} catch (error) {
toast.error('Erro ao criar cliente');
}
}}>
<div className="form-group">
<label className="form-label">Nome Completo *</label>
<input type="text" name="nome_completo" className="form-input" required />
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Email</label>
<input type="email" name="email" className="form-input" />
</div>
<div className="form-group">
<label className="form-label">Telefone</label>
<input type="text" name="telefone" className="form-input" />
</div>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">WhatsApp</label>
<input type="text" name="whatsapp" className="form-input" />
</div>
<div className="form-group">
<label className="form-label">Endereço</label>
<input type="text" name="endereco" className="form-input" />
</div>
</div>
<div className="modal-actions">
<button type="button" className="btn btn-secondary" onClick={() => setShowClienteModal(false)}>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
<FiPlus />
Criar Cliente
</button>
</div>
</form>
</div>
</div>
</div>
)}
{showDespesaModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2>Nova Despesa</h2>
<button
className="btn-icon"
onClick={() => setShowDespesaModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-content">
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
await despesasAPI.criar({
tipo_despesa_id: formData.get('tipo_despesa_id'),
fornecedor_id: formData.get('fornecedor_id') || null,
data_despesa: formData.get('data_despesa'),
valor: parseFloat(formData.get('valor')),
descricao: formData.get('descricao')
});
toast.success('Despesa criada com sucesso!');
setShowDespesaModal(false);
carregarDashboard();
} catch (error) {
toast.error('Erro ao criar despesa');
}
}}>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Tipo de Despesa *</label>
<select name="tipo_despesa_id" className="form-select" required>
<option value="">Selecione o tipo</option>
{tiposDespesas.map(tipo => (
<option key={tipo.id} value={tipo.id}>{tipo.nome}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Fornecedor</label>
<select name="fornecedor_id" className="form-select">
<option value="">Selecione o fornecedor</option>
{fornecedores.map(fornecedor => (
<option key={fornecedor.id} value={fornecedor.id}>{fornecedor.razao_social}</option>
))}
</select>
</div>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Data da Despesa *</label>
<input type="date" name="data_despesa" className="form-input" required />
</div>
<div className="form-group">
<label className="form-label">Valor *</label>
<input type="number" step="0.01" name="valor" className="form-input" required />
</div>
</div>
<div className="form-group">
<label className="form-label">Descrição</label>
<textarea name="descricao" className="form-textarea" rows="3"></textarea>
</div>
<div className="modal-actions">
<button type="button" className="btn btn-secondary" onClick={() => setShowDespesaModal(false)}>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
<FiPlus />
Criar Despesa
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal Novo Produto */}
{showProdutoModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2>Novo Produto</h2>
<button
className="btn-icon"
onClick={() => setShowProdutoModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-content">
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
// Criar FormData para envio
const produtoFormData = new FormData();
produtoFormData.append('id_produto', formData.get('id_produto') || '');
produtoFormData.append('marca', formData.get('marca'));
produtoFormData.append('nome', formData.get('nome'));
produtoFormData.append('estacao', formData.get('estacao'));
produtoFormData.append('genero', formData.get('genero'));
produtoFormData.append('fornecedor_id', formData.get('fornecedor_id') || '');
produtoFormData.append('valor_compra', formData.get('valor_compra'));
produtoFormData.append('valor_revenda', formData.get('valor_revenda'));
// Adicionar variação básica
const variacaoBasica = [{
tamanho: formData.get('tamanho') || 'Único',
cor: formData.get('cor') || 'Padrão',
quantidade: parseInt(formData.get('quantidade')) || 1
}];
produtoFormData.append('variacoes_data', JSON.stringify(variacaoBasica));
try {
const response = await fetch('/api/produtos', {
method: 'POST',
body: produtoFormData
});
if (response.ok) {
toast.success('Produto criado com sucesso!');
setShowProdutoModal(false);
carregarDashboard();
} else {
const error = await response.json();
toast.error(error.error || 'Erro ao criar produto');
}
} catch (error) {
console.error('Erro ao criar produto:', error);
toast.error('Erro ao criar produto');
}
}}>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">ID do Produto</label>
<input type="text" name="id_produto" className="form-input" placeholder="Ex: LK001" />
</div>
<div className="form-group">
<label className="form-label">Marca *</label>
<input type="text" name="marca" className="form-input" required placeholder="Ex: Nike" />
</div>
</div>
<div className="form-group">
<label className="form-label">Nome do Produto *</label>
<input type="text" name="nome" className="form-input" required placeholder="Ex: Camiseta Infantil" />
</div>
<div className="grid grid-3">
<div className="form-group">
<label className="form-label">Estação *</label>
<select name="estacao" className="form-select" required>
<option value="Verão">Verão</option>
<option value="Inverno">Inverno</option>
<option value="Meia Estação">Meia Estação</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Gênero *</label>
<select name="genero" className="form-select" required>
<option value="Menino">Menino</option>
<option value="Menina">Menina</option>
<option value="Unissex">Unissex</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Fornecedor</label>
<select name="fornecedor_id" className="form-select">
<option value="">Selecione</option>
{fornecedores.map(fornecedor => (
<option key={fornecedor.id} value={fornecedor.id}>{fornecedor.razao_social}</option>
))}
</select>
</div>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Valor de Compra *</label>
<input type="number" step="0.01" name="valor_compra" className="form-input" required placeholder="0,00" />
</div>
<div className="form-group">
<label className="form-label">Valor de Revenda *</label>
<input type="number" step="0.01" name="valor_revenda" className="form-input" required placeholder="0,00" />
</div>
</div>
<div className="form-section">
<h4>Variação Básica</h4>
<div className="grid grid-3">
<div className="form-group">
<label className="form-label">Tamanho</label>
<input type="text" name="tamanho" className="form-input" placeholder="Ex: M, 6, 8" />
</div>
<div className="form-group">
<label className="form-label">Cor</label>
<input type="text" name="cor" className="form-input" placeholder="Ex: Azul, Rosa" />
</div>
<div className="form-group">
<label className="form-label">Quantidade</label>
<input type="number" name="quantidade" className="form-input" placeholder="1" min="0" />
</div>
</div>
</div>
<div className="modal-actions">
<button type="button" className="btn btn-secondary" onClick={() => setShowProdutoModal(false)}>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
<FiPlus />
Criar Produto
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal Nova Venda */}
{showVendaModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2>Nova Venda</h2>
<button
className="btn-icon"
onClick={() => setShowVendaModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-content">
<form onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const vendaData = {
cliente_id: formData.get('cliente_id') || null,
tipo_pagamento: formData.get('tipo_pagamento'),
valor_total: parseFloat(formData.get('valor_total')),
desconto: parseFloat(formData.get('desconto')) || 0,
parcelas: parseInt(formData.get('parcelas')) || 1,
valor_parcela: parseFloat(formData.get('valor_parcela')) || 0,
data_venda: formData.get('data_venda'),
observacoes: formData.get('observacoes') || '',
itens: [] // Venda simples sem itens específicos
};
try {
const response = await fetch('/api/vendas', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(vendaData)
});
if (response.ok) {
toast.success('Venda registrada com sucesso!');
setShowVendaModal(false);
carregarDashboard();
} else {
const error = await response.json();
toast.error(error.error || 'Erro ao registrar venda');
}
} catch (error) {
console.error('Erro ao registrar venda:', error);
toast.error('Erro ao registrar venda');
}
}}>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Cliente</label>
<select name="cliente_id" className="form-select">
<option value="">Venda sem cliente cadastrado</option>
{clientes.map(cliente => (
<option key={cliente.id} value={cliente.id}>{cliente.nome_completo}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Data da Venda *</label>
<input
type="date"
name="data_venda"
className="form-input"
required
defaultValue={new Date().toISOString().split('T')[0]}
/>
</div>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Tipo de Pagamento *</label>
<select name="tipo_pagamento" className="form-select" required onChange={(e) => {
const parcelasField = document.querySelector('input[name="parcelas"]');
const valorParcelaField = document.querySelector('input[name="valor_parcela"]');
if (e.target.value === 'vista') {
parcelasField.value = '1';
parcelasField.disabled = true;
valorParcelaField.disabled = true;
} else {
parcelasField.disabled = false;
valorParcelaField.disabled = false;
}
}}>
<option value="">Selecione</option>
<option value="vista">À Vista</option>
<option value="parcelado">Parcelado</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Valor Total *</label>
<input
type="number"
step="0.01"
name="valor_total"
className="form-input"
required
placeholder="0,00"
onChange={(e) => {
const parcelas = parseInt(document.querySelector('input[name="parcelas"]').value) || 1;
const desconto = parseFloat(document.querySelector('input[name="desconto"]').value) || 0;
const valorTotal = parseFloat(e.target.value) || 0;
const valorFinal = valorTotal - desconto;
const valorParcela = valorFinal / parcelas;
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
}}
/>
</div>
</div>
<div className="grid grid-3">
<div className="form-group">
<label className="form-label">Desconto</label>
<input
type="number"
step="0.01"
name="desconto"
className="form-input"
placeholder="0,00"
onChange={(e) => {
const parcelas = parseInt(document.querySelector('input[name="parcelas"]').value) || 1;
const valorTotal = parseFloat(document.querySelector('input[name="valor_total"]').value) || 0;
const desconto = parseFloat(e.target.value) || 0;
const valorFinal = valorTotal - desconto;
const valorParcela = valorFinal / parcelas;
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
}}
/>
</div>
<div className="form-group">
<label className="form-label">Parcelas</label>
<input
type="number"
name="parcelas"
className="form-input"
min="1"
defaultValue="1"
onChange={(e) => {
const valorTotal = parseFloat(document.querySelector('input[name="valor_total"]').value) || 0;
const desconto = parseFloat(document.querySelector('input[name="desconto"]').value) || 0;
const parcelas = parseInt(e.target.value) || 1;
const valorFinal = valorTotal - desconto;
const valorParcela = valorFinal / parcelas;
document.querySelector('input[name="valor_parcela"]').value = valorParcela.toFixed(2);
}}
/>
</div>
<div className="form-group">
<label className="form-label">Valor por Parcela</label>
<input type="number" step="0.01" name="valor_parcela" className="form-input" readOnly />
</div>
</div>
<div className="form-group">
<label className="form-label">Observações</label>
<textarea name="observacoes" className="form-textarea" rows="3" placeholder="Observações sobre a venda..."></textarea>
</div>
<div className="modal-actions">
<button type="button" className="btn btn-secondary" onClick={() => setShowVendaModal(false)}>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
<FiPlus />
Registrar Venda
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,333 @@
import React, { useState, useEffect } from 'react';
import {
FiDollarSign,
FiTrendingUp,
FiTrendingDown,
FiClock,
FiAlertCircle,
FiPackage,
FiUsers,
FiTruck,
FiCreditCard,
FiCalendar,
FiMessageSquare
} from 'react-icons/fi';
import { dashboardAPI } from '../services/api';
import toast from 'react-hot-toast';
import '../styles/dashboard-contabilidade.css';
const DashboardNovo = () => {
const [dashboardData, setDashboardData] = useState({
contabilidade: {
receitaBruta: 0,
custosProdutos: 0,
totalDespesas: 0,
lucroReal: 0,
margemLucro: 0
},
resumoFinanceiro: {
receitasMes: 0,
despesasMes: 0,
lucroEstimado: 0,
totalVendas: 0
},
emprestimos: {
totalAberto: 0,
totalQuitado: 0,
quantidade: 0
},
vendasPrazo: {
total: 0,
quantidade: 0,
vendas: []
},
parcelasPendentes: {
quantidade: 0,
parcelas: []
},
estatisticas: {
totalProdutos: 0,
totalClientes: 0,
totalFornecedores: 0,
estoqueTotal: 0
}
});
const [loading, setLoading] = useState(true);
useEffect(() => {
carregarDashboard();
}, []);
const carregarDashboard = async () => {
try {
setLoading(true);
const response = await dashboardAPI.obterEstatisticas();
setDashboardData(response.data);
} catch (error) {
console.error('Erro ao carregar dashboard:', error);
toast.error('Erro ao carregar dados do dashboard');
} finally {
setLoading(false);
}
};
const formatarMoeda = (valor) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
};
const formatarData = (data) => {
return new Date(data).toLocaleDateString('pt-BR');
};
const enviarWhatsApp = (venda) => {
const telefone = venda.cliente_whatsapp || venda.cliente_telefone;
if (!telefone) {
toast.error('Cliente não possui WhatsApp cadastrado');
return;
}
const numeroLimpo = telefone.replace(/\D/g, '');
const mensagem = `Olá! Sua compra no valor de ${formatarMoeda(venda.valor)} vence em ${formatarData(venda.vencimento)}. Obrigado!`;
const mensagemCodificada = encodeURIComponent(mensagem);
const url = `https://wa.me/55${numeroLimpo}?text=${mensagemCodificada}`;
window.open(url, '_blank');
toast.success('WhatsApp aberto com sucesso!');
};
if (loading) {
return (
<div className="dashboard-loading">
<div className="loading-spinner"></div>
<p>Carregando dashboard...</p>
</div>
);
}
return (
<div className="dashboard">
<div className="dashboard-header">
<h1>Dashboard</h1>
<p>Visão geral do seu negócio</p>
</div>
{/* CONTABILIDADE COMPLETA */}
<div className="contabilidade-section">
<h2>📊 Contabilidade Completa - {new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })}</h2>
<div className="contabilidade-grid">
<div className="contabilidade-card receita">
<div className="card-header">
<FiTrendingUp className="card-icon" />
<span>Receita Bruta</span>
</div>
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.receitaBruta)}</div>
<div className="card-subtitle">Vendas do mês</div>
</div>
<div className="contabilidade-card custo">
<div className="card-header">
<FiPackage className="card-icon" />
<span>Custos dos Produtos</span>
</div>
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.custosProdutos)}</div>
<div className="card-subtitle">Custo das mercadorias vendidas</div>
</div>
<div className="contabilidade-card despesa">
<div className="card-header">
<FiTrendingDown className="card-icon" />
<span>Despesas</span>
</div>
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.totalDespesas)}</div>
<div className="card-subtitle">Gastos operacionais</div>
</div>
<div className="contabilidade-card lucro">
<div className="card-header">
<FiDollarSign className="card-icon" />
<span>Lucro Real</span>
</div>
<div className="card-value">{formatarMoeda(dashboardData.contabilidade.lucroReal)}</div>
<div className="card-subtitle">
Margem: {dashboardData.contabilidade.margemLucro}%
</div>
</div>
</div>
</div>
{/* RESUMO FINANCEIRO */}
<div className="resumo-section">
<h2>💰 Resumo Financeiro</h2>
<div className="resumo-grid">
<div className="resumo-card">
<div className="resumo-header">
<span>Receitas do Mês</span>
<FiTrendingUp className="resumo-icon success" />
</div>
<div className="resumo-value success">
{formatarMoeda(dashboardData.resumoFinanceiro.receitasMes)}
</div>
</div>
<div className="resumo-card">
<div className="resumo-header">
<span>Despesas do Mês</span>
<FiTrendingDown className="resumo-icon danger" />
</div>
<div className="resumo-value danger">
{formatarMoeda(dashboardData.resumoFinanceiro.despesasMes)}
</div>
</div>
<div className="resumo-card">
<div className="resumo-header">
<span>Lucro Estimado</span>
<FiDollarSign className="resumo-icon primary" />
</div>
<div className="resumo-value primary">
{formatarMoeda(dashboardData.resumoFinanceiro.lucroEstimado)}
</div>
</div>
</div>
</div>
{/* VENDAS A PRAZO & RECEBIMENTOS */}
<div className="vendas-prazo-section">
<h2>📅 Vendas a Prazo & Recebimentos</h2>
{dashboardData.vendasPrazo.quantidade > 0 ? (
<div className="vendas-prazo-container">
<div className="vendas-prazo-header">
<div className="prazo-summary">
<span className="prazo-count">{dashboardData.vendasPrazo.quantidade} vendas</span>
<span className="prazo-total">{formatarMoeda(dashboardData.vendasPrazo.total)}</span>
</div>
</div>
<div className="vendas-prazo-list">
{dashboardData.vendasPrazo.vendas.map((venda, index) => (
<div key={index} className="venda-prazo-item">
<div className="venda-info">
<div className="cliente-nome">{venda.cliente}</div>
<div className="venda-valor">{formatarMoeda(venda.valor)}</div>
</div>
<div className="venda-vencimento">
<FiCalendar className="vencimento-icon" />
<span>Vence em: {formatarData(venda.vencimento)}</span>
</div>
<div className="venda-actions">
<button
className="btn-whatsapp"
onClick={() => enviarWhatsApp(venda)}
title="Enviar cobrança via WhatsApp"
>
<FiMessageSquare />
</button>
</div>
</div>
))}
</div>
</div>
) : (
<div className="empty-state">
<FiClock className="empty-icon" />
<p>Nenhuma venda a prazo encontrada</p>
</div>
)}
</div>
{/* PARCELAS PENDENTES */}
{dashboardData.parcelasPendentes.quantidade > 0 && (
<div className="parcelas-section">
<h2>💳 Parcelas Pendentes</h2>
<div className="parcelas-list">
{dashboardData.parcelasPendentes.parcelas.map((parcela, index) => (
<div key={index} className="parcela-item">
<div className="parcela-info">
<div className="parcela-cliente">{parcela.cliente}</div>
<div className="parcela-valor">{formatarMoeda(parcela.valor)}</div>
</div>
<div className="parcela-detalhes">
<span>{parcela.parcelas}x parcelas</span>
<span>Próximo: {formatarData(parcela.proximoVencimento)}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* EMPRÉSTIMOS */}
<div className="emprestimos-section">
<h2>🏦 Empréstimos</h2>
<div className="emprestimos-grid">
<div className="emprestimo-card aberto">
<div className="emprestimo-header">
<FiAlertCircle className="emprestimo-icon" />
<span>Em Aberto</span>
</div>
<div className="emprestimo-value">
{formatarMoeda(dashboardData.emprestimos.totalAberto)}
</div>
<div className="emprestimo-count">
{dashboardData.emprestimos.quantidade} empréstimos
</div>
</div>
<div className="emprestimo-card quitado">
<div className="emprestimo-header">
<FiTrendingUp className="emprestimo-icon" />
<span>Quitados</span>
</div>
<div className="emprestimo-value">
{formatarMoeda(dashboardData.emprestimos.totalQuitado)}
</div>
</div>
</div>
</div>
{/* ESTATÍSTICAS GERAIS */}
<div className="estatisticas-section">
<h2>📈 Estatísticas Gerais</h2>
<div className="estatisticas-grid">
<div className="stat-card">
<FiPackage className="stat-icon" />
<div className="stat-info">
<div className="stat-value">{dashboardData.estatisticas.totalProdutos}</div>
<div className="stat-label">Produtos</div>
</div>
</div>
<div className="stat-card">
<FiUsers className="stat-icon" />
<div className="stat-info">
<div className="stat-value">{dashboardData.estatisticas.totalClientes}</div>
<div className="stat-label">Clientes</div>
</div>
</div>
<div className="stat-card">
<FiTruck className="stat-icon" />
<div className="stat-info">
<div className="stat-value">{dashboardData.estatisticas.totalFornecedores}</div>
<div className="stat-label">Fornecedores</div>
</div>
</div>
<div className="stat-card">
<FiPackage className="stat-icon" />
<div className="stat-info">
<div className="stat-value">{dashboardData.estatisticas.estoqueTotal}</div>
<div className="stat-label">Estoque Total</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default DashboardNovo;

View File

@@ -0,0 +1,246 @@
import React, { useState, useEffect } from 'react';
import {
FiDollarSign,
FiTrendingUp,
FiTrendingDown,
FiClock,
FiPackage,
FiUsers,
FiCalendar,
FiMessageSquare
} from 'react-icons/fi';
import { dashboardAPI } from '../services/api';
import toast from 'react-hot-toast';
import '../styles/dashboard-simples.css';
const DashboardSimples = () => {
const [dashboardData, setDashboardData] = useState({
contabilidade: {
receitaBruta: 0,
custosProdutos: 0,
totalDespesas: 0,
lucroReal: 0,
margemLucro: 0
},
vendasPrazo: {
total: 0,
quantidade: 0,
vendas: []
},
parcelasPendentes: {
quantidade: 0,
parcelas: []
},
estatisticas: {
totalProdutos: 0,
totalClientes: 0,
totalFornecedores: 0,
estoqueTotal: 0
}
});
const [loading, setLoading] = useState(true);
useEffect(() => {
carregarDashboard();
}, []);
const carregarDashboard = async () => {
try {
setLoading(true);
const response = await dashboardAPI.obterEstatisticas();
setDashboardData(response.data);
} catch (error) {
console.error('Erro ao carregar dashboard:', error);
toast.error('Erro ao carregar dados do dashboard');
} finally {
setLoading(false);
}
};
const formatarMoeda = (valor) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
};
const formatarData = (data) => {
return new Date(data).toLocaleDateString('pt-BR');
};
const enviarWhatsApp = (venda) => {
const telefone = venda.cliente_whatsapp || venda.cliente_telefone;
if (!telefone) {
toast.error('Cliente não possui WhatsApp cadastrado');
return;
}
const numeroLimpo = telefone.replace(/\D/g, '');
const mensagem = `Olá! Sua compra no valor de ${formatarMoeda(venda.valor)} vence em ${formatarData(venda.vencimento)}. Obrigado!`;
const mensagemCodificada = encodeURIComponent(mensagem);
const url = `https://wa.me/55${numeroLimpo}?text=${mensagemCodificada}`;
window.open(url, '_blank');
toast.success('WhatsApp aberto com sucesso!');
};
if (loading) {
return (
<div className="dashboard-loading">
<div className="loading-spinner"></div>
<p>Carregando dashboard...</p>
</div>
);
}
const mesAtual = new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
return (
<div className="dashboard-simples">
<div className="dashboard-header">
<h1>Dashboard</h1>
<p>Visão geral do seu negócio - {mesAtual}</p>
</div>
{/* RESUMO FINANCEIRO PRINCIPAL */}
<div className="resumo-principal">
<div className="card-principal receitas">
<div className="card-icon">
<FiTrendingUp />
</div>
<div className="card-content">
<h3>Receitas do Mês</h3>
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.receitaBruta)}</div>
<div className="subtexto">Vendas realizadas</div>
</div>
</div>
<div className="card-principal despesas">
<div className="card-icon">
<FiTrendingDown />
</div>
<div className="card-content">
<h3>Despesas do Mês</h3>
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.totalDespesas)}</div>
<div className="subtexto">Gastos operacionais</div>
</div>
</div>
<div className="card-principal lucro">
<div className="card-icon">
<FiDollarSign />
</div>
<div className="card-content">
<h3>Lucro Real</h3>
<div className="valor-principal">{formatarMoeda(dashboardData.contabilidade.lucroReal)}</div>
<div className="subtexto">Margem: {dashboardData.contabilidade.margemLucro}%</div>
</div>
</div>
</div>
{/* VENDAS A PRAZO */}
{dashboardData.vendasPrazo.quantidade > 0 && (
<div className="secao-vendas-prazo">
<div className="secao-header">
<h2>📅 Vendas a Prazo & Recebimentos</h2>
<div className="total-prazo">
Total: {formatarMoeda(dashboardData.vendasPrazo.total)}
</div>
</div>
<div className="lista-vendas-prazo">
{dashboardData.vendasPrazo.vendas.slice(0, 5).map((venda, index) => (
<div key={index} className="item-venda-prazo">
<div className="venda-info">
<div className="cliente">{venda.cliente}</div>
<div className="valor">{formatarMoeda(venda.valor)}</div>
</div>
<div className="venda-vencimento">
<FiCalendar />
<span>{formatarData(venda.vencimento)}</span>
</div>
<button
className="btn-whatsapp-simples"
onClick={() => enviarWhatsApp(venda)}
title="Enviar WhatsApp"
>
<FiMessageSquare />
</button>
</div>
))}
{dashboardData.vendasPrazo.quantidade > 5 && (
<div className="mais-vendas">
+{dashboardData.vendasPrazo.quantidade - 5} vendas a prazo
</div>
)}
</div>
</div>
)}
{/* PARCELAS PENDENTES */}
{dashboardData.parcelasPendentes.quantidade > 0 && (
<div className="secao-parcelas">
<div className="secao-header">
<h2>💳 Parcelas Pendentes</h2>
<div className="total-parcelas">
{dashboardData.parcelasPendentes.quantidade} parcelas
</div>
</div>
<div className="lista-parcelas">
{dashboardData.parcelasPendentes.parcelas.slice(0, 3).map((parcela, index) => (
<div key={index} className="item-parcela">
<div className="parcela-info">
<div className="cliente">{parcela.cliente}</div>
<div className="valor">{formatarMoeda(parcela.valor)}</div>
</div>
<div className="parcela-detalhes">
<span>{parcela.parcelas}x parcelas</span>
<span>Próximo: {formatarData(parcela.proximoVencimento)}</span>
</div>
</div>
))}
</div>
</div>
)}
{/* ESTATÍSTICAS RÁPIDAS */}
<div className="estatisticas-rapidas">
<div className="stat-item">
<FiPackage className="stat-icon" />
<div className="stat-info">
<div className="stat-numero">{dashboardData.estatisticas.totalProdutos}</div>
<div className="stat-label">Produtos</div>
</div>
</div>
<div className="stat-item">
<FiUsers className="stat-icon" />
<div className="stat-info">
<div className="stat-numero">{dashboardData.estatisticas.totalClientes}</div>
<div className="stat-label">Clientes</div>
</div>
</div>
<div className="stat-item">
<FiPackage className="stat-icon" />
<div className="stat-info">
<div className="stat-numero">{dashboardData.estatisticas.estoqueTotal}</div>
<div className="stat-label">Estoque</div>
</div>
</div>
</div>
{/* EMPTY STATES */}
{dashboardData.vendasPrazo.quantidade === 0 && dashboardData.parcelasPendentes.quantidade === 0 && (
<div className="empty-recebimentos">
<FiClock className="empty-icon" />
<h3>Nenhum recebimento pendente</h3>
<p>Todas as vendas foram pagas à vista</p>
</div>
)}
</div>
);
};
export default DashboardSimples;

View File

@@ -0,0 +1,447 @@
import React, { useState, useEffect } from 'react';
import {
FiPlus,
FiEdit,
FiTrash2,
FiSearch,
FiDollarSign,
FiCalendar,
FiTag
} from 'react-icons/fi';
import { despesasAPI, fornecedoresAPI } from '../services/api';
import ViewToggle from '../components/ViewToggle';
import toast from 'react-hot-toast';
const Despesas = () => {
const [despesas, setDespesas] = useState([]);
const [tiposDespesas, setTiposDespesas] = useState([]);
const [fornecedores, setFornecedores] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [viewMode, setViewMode] = useState('list');
const [showModal, setShowModal] = useState(false);
const [showTipoModal, setShowTipoModal] = useState(false);
const [editingExpense, setEditingExpense] = useState(null);
const [formData, setFormData] = useState({
tipo_despesa: '',
fornecedor: '',
data: new Date().toISOString().split('T')[0],
valor: '',
descricao: ''
});
const [novoTipo, setNovoTipo] = useState('');
useEffect(() => {
carregarDados();
}, []);
const carregarDados = async () => {
try {
setLoading(true);
const [despesasRes, tiposRes, fornecedoresRes] = await Promise.all([
despesasAPI.listar(),
despesasAPI.listarTipos(),
fornecedoresAPI.listar()
]);
setDespesas(despesasRes.data);
setTiposDespesas(tiposRes.data);
setFornecedores(fornecedoresRes.data);
} catch (error) {
console.error('Erro ao carregar dados:', error);
toast.error('Erro ao carregar despesas');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (editingExpense) {
await despesasAPI.atualizar(editingExpense.id, formData);
toast.success('Despesa atualizada com sucesso!');
} else {
await despesasAPI.criar(formData);
toast.success('Despesa cadastrada com sucesso!');
}
setShowModal(false);
setEditingExpense(null);
setFormData({
tipo_despesa: '',
fornecedor: '',
data: new Date().toISOString().split('T')[0],
valor: '',
descricao: ''
});
carregarDados();
} catch (error) {
console.error('Erro ao salvar despesa:', error);
toast.error('Erro ao salvar despesa');
}
};
const handleAddTipo = async (e) => {
e.preventDefault();
try {
await despesasAPI.criarTipo({ nome: novoTipo });
toast.success('Tipo de despesa criado com sucesso!');
setShowTipoModal(false);
setNovoTipo('');
carregarDados();
} catch (error) {
console.error('Erro ao criar tipo de despesa:', error);
toast.error('Erro ao criar tipo de despesa');
}
};
const handleEdit = (despesa) => {
setEditingExpense(despesa);
setFormData({
tipo_despesa: despesa.tipo_nome || '',
fornecedor: despesa.fornecedor_nome || '',
data: despesa.data,
valor: despesa.valor,
descricao: despesa.descricao || ''
});
setShowModal(true);
};
const handleDelete = async (id) => {
if (window.confirm('Tem certeza que deseja excluir esta despesa?')) {
try {
await despesasAPI.deletar(id);
toast.success('Despesa excluída com sucesso!');
carregarDados();
} catch (error) {
console.error('Erro ao excluir despesa:', error);
toast.error('Erro ao excluir despesa');
}
}
};
const filteredDespesas = despesas.filter(despesa =>
(despesa.descricao && despesa.descricao.toLowerCase().includes(searchTerm.toLowerCase())) ||
(despesa.tipo_nome && despesa.tipo_nome.toLowerCase().includes(searchTerm.toLowerCase())) ||
(despesa.fornecedor_nome && despesa.fornecedor_nome.toLowerCase().includes(searchTerm.toLowerCase()))
);
const totalDespesas = filteredDespesas.reduce((total, despesa) => total + parseFloat(despesa.valor), 0);
if (loading) {
return (
<div className="loading">
<div>Carregando despesas...</div>
</div>
);
}
return (
<div className="despesas fade-in">
<div className="page-header">
<div>
<h1>Despesas</h1>
<p>Controle todos os gastos da empresa</p>
</div>
<div className="header-actions">
<ViewToggle
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<button
className="btn btn-secondary"
onClick={() => setShowTipoModal(true)}
>
<FiTag />
Tipos de Despesa
</button>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Nova Despesa
</button>
</div>
</div>
{/* Resumo */}
<div className="expenses-summary">
<div className="summary-card">
<div className="summary-icon">
<FiDollarSign />
</div>
<div className="summary-content">
<div className="summary-value">
R$ {totalDespesas.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</div>
<div className="summary-label">Total em Despesas</div>
</div>
</div>
</div>
{/* Barra de Pesquisa */}
<div className="search-box">
<FiSearch className="search-icon" />
<input
type="text"
placeholder="Pesquisar despesas..."
className="search-input"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Lista de Despesas */}
{filteredDespesas.length === 0 ? (
<div className="empty-state">
<FiDollarSign size={48} />
<h3>Nenhuma despesa encontrada</h3>
<p>Comece registrando sua primeira despesa</p>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Adicionar Despesa
</button>
</div>
) : (
<div className="expenses-table">
<table className="table">
<thead>
<tr>
<th>Data</th>
<th>Tipo</th>
<th>Fornecedor</th>
<th>Descrição</th>
<th>Valor</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{filteredDespesas.map((despesa) => (
<tr key={despesa.id}>
<td>
<div className="expense-date">
<FiCalendar className="date-icon" />
{new Date(despesa.data).toLocaleDateString('pt-BR')}
</div>
</td>
<td>
<span className="badge badge-info">
{despesa.tipo_nome}
</span>
</td>
<td>{despesa.fornecedor_nome || '-'}</td>
<td>
<div className="expense-description">
{despesa.descricao || '-'}
</div>
</td>
<td>
<span className="expense-value">
R$ {parseFloat(despesa.valor).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
</span>
</td>
<td>
<div className="table-actions">
<button
className="btn-icon"
onClick={() => handleEdit(despesa)}
title="Editar"
>
<FiEdit />
</button>
<button
className="btn-icon btn-danger"
onClick={() => handleDelete(despesa.id)}
title="Excluir"
>
<FiTrash2 />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Modal de Despesa */}
{showModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">
{editingExpense ? 'Editar Despesa' : 'Nova Despesa'}
</h2>
<button
className="modal-close"
onClick={() => {
setShowModal(false);
setEditingExpense(null);
setFormData({
tipo_despesa: '',
fornecedor: '',
data: new Date().toISOString().split('T')[0],
valor: '',
descricao: ''
});
}}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Tipo de Despesa *</label>
<input
type="text"
className="form-input"
value={formData.tipo_despesa}
onChange={(e) => setFormData({...formData, tipo_despesa: e.target.value})}
placeholder="Ex: Aluguel, Energia, Marketing..."
required
/>
</div>
<div className="form-group">
<label className="form-label">Fornecedor</label>
<input
type="text"
className="form-input"
value={formData.fornecedor}
onChange={(e) => setFormData({...formData, fornecedor: e.target.value})}
placeholder="Nome do fornecedor..."
/>
</div>
<div className="form-group">
<label className="form-label">Data *</label>
<input
type="date"
className="form-input"
value={formData.data}
onChange={(e) => setFormData({...formData, data: e.target.value})}
required
/>
</div>
<div className="form-group">
<label className="form-label">Valor *</label>
<input
type="number"
step="0.01"
className="form-input"
value={formData.valor}
onChange={(e) => setFormData({...formData, valor: e.target.value})}
required
/>
</div>
</div>
<div className="form-group">
<label className="form-label">Descrição</label>
<textarea
className="form-textarea"
value={formData.descricao}
onChange={(e) => setFormData({...formData, descricao: e.target.value})}
placeholder="Descreva a despesa..."
rows="3"
/>
</div>
<div className="modal-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowModal(false);
setEditingExpense(null);
}}
>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
{editingExpense ? 'Atualizar' : 'Cadastrar'} Despesa
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal de Tipo de Despesa */}
{showTipoModal && (
<div className="modal-overlay">
<div className="modal modal-sm">
<div className="modal-header">
<h2 className="modal-title">Novo Tipo de Despesa</h2>
<button
className="modal-close"
onClick={() => {
setShowTipoModal(false);
setNovoTipo('');
}}
>
×
</button>
</div>
<form onSubmit={handleAddTipo}>
<div className="form-group">
<label className="form-label">Nome do Tipo *</label>
<input
type="text"
className="form-input"
value={novoTipo}
onChange={(e) => setNovoTipo(e.target.value)}
placeholder="Ex: Aluguel, Energia, Marketing..."
required
/>
</div>
<div className="modal-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowTipoModal(false);
setNovoTipo('');
}}
>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
Criar Tipo
</button>
</div>
</form>
{/* Lista de tipos existentes */}
{tiposDespesas.length > 0 && (
<div className="existing-types">
<h4>Tipos Existentes:</h4>
<div className="types-list">
{tiposDespesas.map((tipo) => (
<span key={tipo.id} className="badge badge-info">
{tipo.nome}
</span>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
export default Despesas;

View File

@@ -0,0 +1,739 @@
import React, { useState, useEffect } from 'react';
import {
FiArrowLeft,
FiPackage,
FiCalendar,
FiUser,
FiDollarSign,
FiRotateCcw,
FiCheck,
FiX,
FiAlertTriangle,
FiPlus
} from 'react-icons/fi';
import toast from 'react-hot-toast';
import '../styles/devolucoes.css';
const Devolucoes = () => {
const [vendas, setVendas] = useState([]);
const [loading, setLoading] = useState(true);
const [vendaSelecionada, setVendaSelecionada] = useState(null);
const [showModal, setShowModal] = useState(false);
const [showHistoricoModal, setShowHistoricoModal] = useState(false);
const [historicoVenda, setHistoricoVenda] = useState([]);
const [itensDevolucao, setItensDevolucao] = useState([]);
const [motivo, setMotivo] = useState('');
const [processando, setProcessando] = useState(false);
const [tipoOperacao, setTipoOperacao] = useState('devolucao');
const [produtos, setProdutos] = useState([]);
const [itensTroca, setItensTroca] = useState([]);
useEffect(() => {
carregarVendas();
}, []);
const carregarVendas = async () => {
try {
setLoading(true);
const response = await fetch('/api/devolucoes/vendas');
if (response.ok) {
const data = await response.json();
setVendas(data);
} else {
toast.error('Erro ao carregar vendas');
}
} catch (error) {
console.error('Erro ao carregar vendas:', error);
toast.error('Erro ao carregar vendas');
} finally {
setLoading(false);
}
};
const carregarHistoricoVenda = async (vendaId) => {
try {
const response = await fetch(`/api/devolucoes/venda/${vendaId}`);
if (response.ok) {
const historico = await response.json();
setHistoricoVenda(historico);
setShowHistoricoModal(true);
} else {
toast.error('Erro ao carregar histórico');
}
} catch (error) {
console.error('Erro ao carregar histórico:', error);
toast.error('Erro ao carregar histórico');
}
};
const carregarProdutos = async () => {
try {
const response = await fetch('/api/devolucoes/produtos');
if (response.ok) {
const data = await response.json();
setProdutos(data);
}
} catch (error) {
console.error('Erro ao carregar produtos:', error);
}
};
const abrirModalDevolucao = (venda) => {
setVendaSelecionada(venda);
setItensDevolucao(venda.itens.map(item => ({
...item,
quantidade_devolver: 0,
selecionado: false
})));
setItensTroca([]);
setMotivo('');
setTipoOperacao('devolucao');
carregarProdutos();
setShowModal(true);
};
const handleItemChange = (itemId, quantidade) => {
setItensDevolucao(prev => prev.map(item => {
if (item.id === itemId) {
const quantidadeDevolver = Math.min(Math.max(0, parseInt(quantidade) || 0), item.quantidade);
return {
...item,
quantidade_devolver: quantidadeDevolver,
selecionado: quantidadeDevolver > 0
};
}
return item;
}));
};
const calcularValorDevolucao = () => {
return itensDevolucao.reduce((total, item) => {
if (item.selecionado && item.quantidade_devolver > 0) {
return total + (parseFloat(item.valor_unitario) * item.quantidade_devolver);
}
return total;
}, 0);
};
const calcularValorTroca = () => {
return itensTroca.reduce((total, item) => {
return total + (parseFloat(item.valor_unitario || 0) * parseInt(item.quantidade || 0));
}, 0);
};
const adicionarItemTroca = () => {
setItensTroca(prev => [...prev, {
id: Date.now(),
produto_id: '',
variacao_id: '',
quantidade: 1,
valor_unitario: 0,
produto_nome: '',
variacao_info: ''
}]);
};
const removerItemTroca = (id) => {
setItensTroca(prev => prev.filter(item => item.id !== id));
};
const handleTrocaChange = (id, field, value) => {
setItensTroca(prev => prev.map(item => {
if (item.id === id) {
if (field === 'produto_id') {
const produto = produtos.find(p => p.id === value);
return {
...item,
produto_id: value,
produto_nome: produto ? `${produto.marca} - ${produto.nome}` : '',
variacao_id: '',
variacao_info: '',
valor_unitario: produto ? produto.valor_revenda : 0
};
} else if (field === 'variacao_id') {
const produto = produtos.find(p => p.id === item.produto_id);
const variacao = produto?.variacoes.find(v => v.id === value);
return {
...item,
variacao_id: value,
variacao_info: variacao ? `${variacao.tamanho} - ${variacao.cor}` : '',
valor_unitario: variacao ? variacao.preco_venda || produto.valor_revenda : item.valor_unitario
};
} else if (field === 'quantidade') {
const produto = produtos.find(p => p.id === item.produto_id);
const variacao = produto?.variacoes.find(v => v.id === item.variacao_id);
const quantidadeSolicitada = parseInt(value) || 0;
if (variacao && quantidadeSolicitada > variacao.quantidade) {
toast.error(`Estoque insuficiente! Disponível: ${variacao.quantidade}, solicitado: ${quantidadeSolicitada}`);
return { ...item, [field]: variacao.quantidade };
}
return { ...item, [field]: value };
} else {
return { ...item, [field]: value };
}
}
return item;
}));
};
const processarDevolucao = async () => {
const itensSelecionados = itensDevolucao.filter(item => item.selecionado && item.quantidade_devolver > 0);
if (tipoOperacao === 'devolucao' && itensSelecionados.length === 0) {
toast.error('Selecione pelo menos um item para devolução');
return;
}
if (tipoOperacao === 'troca') {
if (itensSelecionados.length === 0) {
toast.error('Selecione pelo menos um item para devolver na troca');
return;
}
const itensValidosTroca = itensTroca.filter(item =>
item.produto_id && item.variacao_id && item.quantidade > 0
);
if (itensValidosTroca.length === 0) {
toast.error('Adicione pelo menos um produto para troca');
return;
}
}
if (!motivo.trim()) {
toast.error(`Informe o motivo da ${tipoOperacao}`);
return;
}
try {
setProcessando(true);
const requestBody = {
venda_id: vendaSelecionada.id,
tipo_operacao: tipoOperacao,
motivo: motivo.trim()
};
if (itensSelecionados.length > 0) {
requestBody.itens_devolucao = itensSelecionados.map(item => ({
item_id: item.id,
quantidade_devolvida: item.quantidade_devolver
}));
}
if (tipoOperacao === 'troca' && itensTroca.length > 0) {
requestBody.itens_troca = itensTroca
.filter(item => item.produto_id && item.variacao_id && item.quantidade > 0)
.map(item => ({
produto_id: item.produto_id,
variacao_id: item.variacao_id,
quantidade: parseInt(item.quantidade),
valor_unitario: parseFloat(item.valor_unitario)
}));
}
const response = await fetch('/api/devolucoes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
const result = await response.json();
toast.success(result.message);
setShowModal(false);
carregarVendas();
} else {
const error = await response.json();
toast.error(error.message || `Erro ao processar ${tipoOperacao}`);
}
} catch (error) {
console.error(`Erro ao processar ${tipoOperacao}:`, error);
toast.error(`Erro ao processar ${tipoOperacao}`);
} finally {
setProcessando(false);
}
};
const formatarMoeda = (valor) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
};
const formatarData = (data) => {
return new Date(data).toLocaleDateString('pt-BR');
};
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>Carregando vendas...</p>
</div>
);
}
return (
<div className="devolucoes-page">
<div className="page-header">
<div className="header-content">
<div className="header-info">
<h1>
<FiRotateCcw className="page-icon" />
Devolução/Troca
</h1>
<p>Gerencie devoluções e trocas de produtos vendidos</p>
</div>
</div>
</div>
<div className="vendas-container">
{vendas.length === 0 ? (
<div className="empty-state">
<FiPackage className="empty-icon" />
<h3>Nenhuma venda encontrada</h3>
<p>Não vendas dos últimos 30 dias disponíveis para devolução</p>
</div>
) : (
<div className="vendas-grid">
{vendas.map(venda => (
<div key={venda.id} className="venda-card">
<div className="venda-header">
<div className="venda-info">
<h3>Venda #{venda.id_venda || venda.id.slice(-8)}</h3>
<div className="venda-meta">
<span className="venda-data">
<FiCalendar />
{formatarData(venda.data_venda)}
</span>
<span className="venda-cliente">
<FiUser />
{venda.cliente_nome}
</span>
</div>
</div>
<div className="venda-valor">
{formatarMoeda(venda.valor_total)}
</div>
</div>
<div className="venda-itens">
<h4>Produtos ({venda.itens.length})</h4>
<div className="itens-lista">
{venda.itens.slice(0, 3).map(item => (
<div key={item.id} className="item-resumo">
<div className="item-info-completa">
<span className="item-nome">{item.produto_nome}</span>
<span className="item-variacao">{item.variacao_info}</span>
<div className="item-detalhes">
<span className="item-quantidade">Qtd: {item.quantidade}</span>
<span className="item-valor">R$ {parseFloat(item.valor_unitario).toFixed(2)}</span>
</div>
</div>
</div>
))}
{venda.itens.length > 3 && (
<div className="item-resumo mais-itens">
+{venda.itens.length - 3} produtos
</div>
)}
</div>
</div>
<div className="venda-actions">
<button
className="btn-detalhes"
onClick={() => carregarHistoricoVenda(venda.id)}
title="Ver histórico de devoluções/trocas"
>
<FiPackage />
Detalhes
</button>
<button
className="btn-devolucao"
onClick={() => abrirModalDevolucao(venda)}
>
<FiRotateCcw />
Devolução/Troca
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Modal de Devolução */}
{showModal && vendaSelecionada && (
<div className="modal-overlay">
<div className="modal-devolucao">
<div className="modal-header">
<h2>
<FiRotateCcw />
Devolução/Troca
</h2>
<button
className="modal-close"
onClick={() => setShowModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-body">
<div className="venda-info-modal">
<h3>Venda #{vendaSelecionada.id_venda || vendaSelecionada.id.slice(-8)}</h3>
<div className="info-grid">
<div className="info-item">
<FiCalendar />
<span>{formatarData(vendaSelecionada.data_venda)}</span>
</div>
<div className="info-item">
<FiUser />
<span>{vendaSelecionada.cliente_nome}</span>
</div>
<div className="info-item">
<FiDollarSign />
<span>{formatarMoeda(vendaSelecionada.valor_total)}</span>
</div>
</div>
</div>
<div className="tipo-operacao">
<h4>Tipo de Operação:</h4>
<div className="radio-group">
<label className="radio-option">
<input
type="radio"
value="devolucao"
checked={tipoOperacao === 'devolucao'}
onChange={(e) => setTipoOperacao(e.target.value)}
/>
<span>Devolução (retorno do dinheiro)</span>
</label>
<label className="radio-option">
<input
type="radio"
value="troca"
checked={tipoOperacao === 'troca'}
onChange={(e) => setTipoOperacao(e.target.value)}
/>
<span>Troca (por outro produto/tamanho)</span>
</label>
</div>
</div>
<div className="itens-devolucao">
<h4>Selecione os itens para devolução:</h4>
<div className="itens-lista-modal">
{itensDevolucao.map(item => (
<div key={item.id} className="item-devolucao">
<div className="item-info">
{item.produto_foto && (
<div className="item-foto">
<img src={item.produto_foto} alt={item.produto_nome} />
</div>
)}
<div className="item-dados">
<div className="item-nome">{item.produto_nome}</div>
<div className="item-codigo">Código: {item.produto_codigo || 'N/A'}</div>
<div className="item-detalhes">
<span className="variacao-badge">{item.variacao_info}</span>
<span className="valor-badge">{formatarMoeda(item.valor_unitario)}</span>
<span className="quantidade-badge">Qtd: {item.quantidade}</span>
</div>
</div>
</div>
<div className="item-controles">
<label>Qtd. a devolver:</label>
<input
type="number"
min="0"
max={item.quantidade}
value={item.quantidade_devolver}
onChange={(e) => handleItemChange(item.id, e.target.value)}
className="quantidade-input"
/>
<div className="valor-item">
{item.quantidade_devolver > 0 && (
<span className="valor-devolucao">
= {formatarMoeda(item.valor_unitario * item.quantidade_devolver)}
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Seção de Troca */}
{tipoOperacao === 'troca' && (
<div className="itens-troca">
<div className="troca-header">
<h4>Produtos para Troca:</h4>
<button
type="button"
className="btn-adicionar-item"
onClick={adicionarItemTroca}
>
<FiPlus />
Adicionar Produto
</button>
</div>
<div className="lista-troca">
{itensTroca.map(item => (
<div key={item.id} className="item-troca">
<div className="troca-selects">
<div className="select-group">
<label>Produto:</label>
<select
value={item.produto_id}
onChange={(e) => handleTrocaChange(item.id, 'produto_id', e.target.value)}
className="select-produto"
>
<option value="">Selecione um produto</option>
{produtos.map(produto => (
<option key={produto.id} value={produto.id}>
{produto.marca} - {produto.nome}
</option>
))}
</select>
</div>
{item.produto_id && (
<div className="select-group">
<label>Variação:</label>
<select
value={item.variacao_id}
onChange={(e) => handleTrocaChange(item.id, 'variacao_id', e.target.value)}
className="select-variacao"
>
<option value="">Selecione uma variação</option>
{produtos.find(p => p.id === item.produto_id)?.variacoes.map(variacao => (
<option key={variacao.id} value={variacao.id}>
{variacao.tamanho} - {variacao.cor} (Estoque: {variacao.quantidade})
</option>
))}
</select>
</div>
)}
<div className="select-group">
<label>Quantidade:</label>
<input
type="number"
min="1"
value={item.quantidade}
onChange={(e) => handleTrocaChange(item.id, 'quantidade', e.target.value)}
className="input-quantidade"
/>
</div>
<div className="select-group">
<label>Valor Unit.:</label>
<input
type="number"
step="0.01"
value={item.valor_unitario}
onChange={(e) => handleTrocaChange(item.id, 'valor_unitario', e.target.value)}
className="input-valor"
/>
</div>
</div>
<div className="troca-actions">
<div className="valor-total-item">
Total: {formatarMoeda(item.valor_unitario * item.quantidade)}
</div>
<button
type="button"
className="btn-remover-item"
onClick={() => removerItemTroca(item.id)}
>
<FiX />
</button>
</div>
</div>
))}
{itensTroca.length === 0 && (
<div className="empty-troca">
<p>Nenhum produto adicionado para troca</p>
</div>
)}
</div>
</div>
)}
<div className="motivo-devolucao">
<label>Motivo da {tipoOperacao} *</label>
<textarea
value={motivo}
onChange={(e) => setMotivo(e.target.value)}
placeholder={`Descreva o motivo da ${tipoOperacao}...`}
rows="3"
className="motivo-textarea"
/>
</div>
<div className="resumo-devolucao">
{tipoOperacao === 'devolucao' ? (
<>
<div className="resumo-item">
<span>Valor total da devolução:</span>
<span className="valor-total">{formatarMoeda(calcularValorDevolucao())}</span>
</div>
<div className="resumo-item">
<span>Novo valor da venda:</span>
<span className="novo-valor">
{formatarMoeda(vendaSelecionada.valor_total - calcularValorDevolucao())}
</span>
</div>
</>
) : (
<>
<div className="resumo-item">
<span>Valor dos itens devolvidos:</span>
<span className="valor-devolucao">{formatarMoeda(calcularValorDevolucao())}</span>
</div>
<div className="resumo-item">
<span>Valor dos itens de troca:</span>
<span className="valor-troca">{formatarMoeda(calcularValorTroca())}</span>
</div>
<div className="resumo-item">
<span>Diferença:</span>
<span className={`diferenca-valor ${calcularValorTroca() - calcularValorDevolucao() >= 0 ? 'positiva' : 'negativa'}`}>
{calcularValorTroca() - calcularValorDevolucao() >= 0 ? '+' : ''}
{formatarMoeda(calcularValorTroca() - calcularValorDevolucao())}
</span>
</div>
<div className="resumo-item">
<span>Novo valor da venda:</span>
<span className="novo-valor">
{formatarMoeda(vendaSelecionada.valor_total + (calcularValorTroca() - calcularValorDevolucao()))}
</span>
</div>
</>
)}
</div>
</div>
<div className="modal-footer">
<button
className="btn-cancelar"
onClick={() => setShowModal(false)}
disabled={processando}
>
<FiX />
Cancelar
</button>
<button
className="btn-processar"
onClick={processarDevolucao}
disabled={processando || (tipoOperacao === 'devolucao' && calcularValorDevolucao() === 0)}
>
{processando ? (
<>
<div className="spinner-small"></div>
Processando...
</>
) : (
<>
<FiCheck />
{tipoOperacao === 'devolucao' ? 'Processar Devolução' : 'Processar Troca'}
</>
)}
</button>
</div>
</div>
</div>
)}
{/* Modal de Histórico de Devoluções/Trocas */}
{showHistoricoModal && (
<div className="modal-overlay">
<div className="modal-content modal-historico">
<div className="modal-header">
<h2>📋 Histórico de Devoluções/Trocas</h2>
<button
className="btn-fechar"
onClick={() => setShowHistoricoModal(false)}
>
<FiX />
</button>
</div>
<div className="modal-body">
{historicoVenda.length === 0 ? (
<div className="historico-vazio">
<FiPackage size={48} />
<h3>Nenhuma devolução ou troca encontrada</h3>
<p>Esta venda ainda não teve nenhuma devolução ou troca.</p>
</div>
) : (
<div className="historico-lista">
{historicoVenda.map((evento, index) => (
<div key={index} className="historico-item">
<div className="historico-header">
<div className="evento-tipo">
<span className={`badge ${evento.tipo_operacao === 'troca' ? 'badge-warning' : 'badge-info'}`}>
{evento.tipo_operacao === 'troca' ? '🔄 TROCA' : '↩️ DEVOLUÇÃO'}
</span>
<span className="evento-data">
📅 {new Date(evento.data_devolucao).toLocaleDateString('pt-BR')} às {new Date(evento.data_devolucao).toLocaleTimeString('pt-BR', {hour: '2-digit', minute: '2-digit'})}
</span>
</div>
</div>
<div className="evento-produto">
<h4>🔙 Produto {evento.tipo_operacao === 'troca' ? 'Trocado' : 'Devolvido'}:</h4>
{evento.produto_info && (
<div className="produto-card">
<div className="produto-nome">{evento.produto_info.nome}</div>
<div className="produto-detalhes">
<span>📦 Código: {evento.produto_info.codigo || 'N/A'}</span>
<span>📏 Variação: {evento.produto_info.variacao}</span>
<span>🔢 Qtd. Devolvida: {evento.quantidade_devolvida}</span>
<span>💸 Valor: R$ {parseFloat(evento.valor_devolucao).toFixed(2)}</span>
</div>
</div>
)}
</div>
{evento.motivo && (
<div className="evento-motivo">
<h4>📝 Motivo:</h4>
<div className="motivo-texto">{evento.motivo}</div>
</div>
)}
</div>
))}
</div>
)}
</div>
<div className="modal-footer">
<button
className="btn-cancelar"
onClick={() => setShowHistoricoModal(false)}
>
<FiX />
Fechar
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Devolucoes;

View File

@@ -0,0 +1,520 @@
import React, { useState, useEffect } from 'react';
import {
FiPlus,
FiEdit2,
FiTrash2,
FiDollarSign,
FiCalendar,
FiUser,
FiCheckCircle,
FiClock,
FiArrowLeft,
FiEye
} from 'react-icons/fi';
import toast from 'react-hot-toast';
const Emprestimos = () => {
const [emprestimos, setEmprestimos] = useState([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [showDevolucaoModal, setShowDevolucaoModal] = useState(false);
const [emprestimoSelecionado, setEmprestimoSelecionado] = useState(null);
const [editando, setEditando] = useState(false);
const [formData, setFormData] = useState({
pessoa: 'Maiara',
valor_total: '',
descricao: '',
data_emprestimo: new Date().toISOString().split('T')[0]
});
const [devolucaoData, setDevolucaoData] = useState({
valor_devolvido: '',
data_devolucao: new Date().toISOString().split('T')[0],
observacoes: ''
});
useEffect(() => {
carregarEmprestimos();
}, []);
const carregarEmprestimos = async () => {
try {
setLoading(true);
const response = await fetch('/api/emprestimos');
const data = await response.json();
setEmprestimos(data);
} catch (error) {
console.error('Erro ao carregar empréstimos:', error);
toast.error('Erro ao carregar empréstimos');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const url = editando ? `/api/emprestimos/${emprestimoSelecionado.id}` : '/api/emprestimos';
const method = editando ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (response.ok) {
toast.success(result.message);
setShowModal(false);
resetForm();
carregarEmprestimos();
} else {
toast.error(result.error);
}
} catch (error) {
console.error('Erro ao salvar empréstimo:', error);
toast.error('Erro ao salvar empréstimo');
}
};
const handleDevolucao = async (e) => {
e.preventDefault();
try {
const response = await fetch(`/api/emprestimos/${emprestimoSelecionado.id}/devolucoes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(devolucaoData)
});
const result = await response.json();
if (response.ok) {
toast.success(result.message);
setShowDevolucaoModal(false);
setDevolucaoData({
valor_devolvido: '',
data_devolucao: new Date().toISOString().split('T')[0],
observacoes: ''
});
carregarEmprestimos();
} else {
toast.error(result.error);
}
} catch (error) {
console.error('Erro ao registrar devolução:', error);
toast.error('Erro ao registrar devolução');
}
};
const excluirEmprestimo = async (id) => {
if (!window.confirm('Tem certeza que deseja excluir este empréstimo?')) return;
try {
const response = await fetch(`/api/emprestimos/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok) {
toast.success(result.message);
carregarEmprestimos();
} else {
toast.error(result.error);
}
} catch (error) {
console.error('Erro ao excluir empréstimo:', error);
toast.error('Erro ao excluir empréstimo');
}
};
const resetForm = () => {
setFormData({
pessoa: 'Maiara',
valor_total: '',
descricao: '',
data_emprestimo: new Date().toISOString().split('T')[0]
});
setEditando(false);
setEmprestimoSelecionado(null);
};
const abrirModalEdicao = (emprestimo) => {
setFormData({
pessoa: emprestimo.pessoa,
valor_total: emprestimo.valor_total,
descricao: emprestimo.descricao || '',
data_emprestimo: emprestimo.data_emprestimo
});
setEmprestimoSelecionado(emprestimo);
setEditando(true);
setShowModal(true);
};
const abrirModalDevolucao = (emprestimo) => {
setEmprestimoSelecionado(emprestimo);
setDevolucaoData({
valor_devolvido: emprestimo.valor_restante.toString(),
data_devolucao: new Date().toISOString().split('T')[0],
observacoes: ''
});
setShowDevolucaoModal(true);
};
const formatarMoeda = (valor) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
};
const formatarData = (data) => {
return new Date(data + 'T00:00:00').toLocaleDateString('pt-BR');
};
const getStatusBadge = (status) => {
const badges = {
ativo: { color: 'orange', icon: FiClock, text: 'Ativo' },
quitado: { color: 'green', icon: FiCheckCircle, text: 'Quitado' },
cancelado: { color: 'red', icon: FiTrash2, text: 'Cancelado' }
};
const badge = badges[status] || badges.ativo;
const Icon = badge.icon;
return (
<span className={`badge badge-${badge.color}`}>
<Icon size={12} />
{badge.text}
</span>
);
};
const calcularTotalEmprestado = () => {
return emprestimos
.filter(emp => emp.status === 'ativo')
.reduce((total, emp) => total + parseFloat(emp.valor_restante), 0);
};
const calcularTotalQuitado = () => {
return emprestimos
.filter(emp => emp.status === 'quitado')
.reduce((total, emp) => total + parseFloat(emp.valor_total), 0);
};
if (loading) {
return (
<div className="loading">
<div>Carregando empréstimos...</div>
</div>
);
}
return (
<div className="emprestimos fade-in">
<div className="page-header">
<div>
<h1>Controle de Empréstimos</h1>
<p>Gerencie os empréstimos da Maiara</p>
</div>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus /> Novo Empréstimo
</button>
</div>
{/* Cards de Resumo */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon orange">
<FiDollarSign />
</div>
<div className="stat-content">
<h3>Total em Aberto</h3>
<p className="stat-value">{formatarMoeda(calcularTotalEmprestado())}</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon green">
<FiCheckCircle />
</div>
<div className="stat-content">
<h3>Total Quitado</h3>
<p className="stat-value">{formatarMoeda(calcularTotalQuitado())}</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon blue">
<FiUser />
</div>
<div className="stat-content">
<h3>Empréstimos Ativos</h3>
<p className="stat-value">{emprestimos.filter(emp => emp.status === 'ativo').length}</p>
</div>
</div>
</div>
{/* Lista de Empréstimos */}
<div className="content-card">
<div className="card-header">
<h2>Lista de Empréstimos</h2>
</div>
{emprestimos.length === 0 ? (
<div className="empty-state">
<FiDollarSign size={48} />
<h3>Nenhum empréstimo encontrado</h3>
<p>Clique em "Novo Empréstimo" para começar</p>
</div>
) : (
<div className="table-container">
<table className="table">
<thead>
<tr>
<th>Pessoa</th>
<th>Descrição</th>
<th>Valor Total</th>
<th>Valor Restante</th>
<th>Data</th>
<th>Status</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{emprestimos.map((emprestimo) => (
<tr key={emprestimo.id}>
<td>
<div className="user-info">
<FiUser className="user-icon" />
<strong>{emprestimo.pessoa}</strong>
</div>
</td>
<td>
<span className="description-text">
{emprestimo.descricao || '-'}
</span>
</td>
<td>
<strong>{formatarMoeda(emprestimo.valor_total)}</strong>
</td>
<td>
<span className={emprestimo.valor_restante > 0 ? 'text-orange' : 'text-green'}>
<strong>{formatarMoeda(emprestimo.valor_restante)}</strong>
</span>
</td>
<td>{formatarData(emprestimo.data_emprestimo)}</td>
<td>{getStatusBadge(emprestimo.status)}</td>
<td>
<div className="action-buttons">
{emprestimo.status === 'ativo' && emprestimo.valor_restante > 0 && (
<button
className="btn btn-sm btn-success"
onClick={() => abrirModalDevolucao(emprestimo)}
title="Registrar Devolução"
>
<FiDollarSign />
</button>
)}
<button
className="btn btn-sm btn-outline"
onClick={() => abrirModalEdicao(emprestimo)}
title="Editar"
>
<FiEdit2 />
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => excluirEmprestimo(emprestimo.id)}
title="Excluir"
>
<FiTrash2 />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Modal Novo/Editar Empréstimo */}
{showModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h3>{editando ? 'Editar Empréstimo' : 'Novo Empréstimo'}</h3>
<button
className="modal-close"
onClick={() => {
setShowModal(false);
resetForm();
}}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="form-grid">
<div className="form-group">
<label className="form-label">Pessoa</label>
<input
type="text"
className="form-input"
value={formData.pessoa}
onChange={(e) => setFormData({...formData, pessoa: e.target.value})}
required
/>
</div>
<div className="form-group">
<label className="form-label">Valor Total</label>
<input
type="number"
step="0.01"
className="form-input"
value={formData.valor_total}
onChange={(e) => setFormData({...formData, valor_total: e.target.value})}
required
/>
</div>
<div className="form-group">
<label className="form-label">Data do Empréstimo</label>
<input
type="date"
className="form-input"
value={formData.data_emprestimo}
onChange={(e) => setFormData({...formData, data_emprestimo: e.target.value})}
required
/>
</div>
<div className="form-group form-group-full">
<label className="form-label">Descrição</label>
<textarea
className="form-input"
rows="3"
value={formData.descricao}
onChange={(e) => setFormData({...formData, descricao: e.target.value})}
placeholder="Motivo do empréstimo (opcional)"
/>
</div>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-outline"
onClick={() => {
setShowModal(false);
resetForm();
}}
>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
{editando ? 'Atualizar' : 'Criar'} Empréstimo
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal Devolução */}
{showDevolucaoModal && emprestimoSelecionado && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h3>Registrar Devolução</h3>
<button
className="modal-close"
onClick={() => setShowDevolucaoModal(false)}
>
×
</button>
</div>
<form onSubmit={handleDevolucao}>
<div className="modal-body">
<div className="emprestimo-info">
<h4>{emprestimoSelecionado.pessoa}</h4>
<p>Valor restante: <strong>{formatarMoeda(emprestimoSelecionado.valor_restante)}</strong></p>
</div>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Valor da Devolução</label>
<input
type="number"
step="0.01"
className="form-input"
value={devolucaoData.valor_devolvido}
onChange={(e) => setDevolucaoData({...devolucaoData, valor_devolvido: e.target.value})}
max={emprestimoSelecionado.valor_restante}
required
/>
<small className="form-help">
Máximo: {formatarMoeda(emprestimoSelecionado.valor_restante)}
</small>
</div>
<div className="form-group">
<label className="form-label">Data da Devolução</label>
<input
type="date"
className="form-input"
value={devolucaoData.data_devolucao}
onChange={(e) => setDevolucaoData({...devolucaoData, data_devolucao: e.target.value})}
required
/>
</div>
<div className="form-group form-group-full">
<label className="form-label">Observações</label>
<textarea
className="form-input"
rows="3"
value={devolucaoData.observacoes}
onChange={(e) => setDevolucaoData({...devolucaoData, observacoes: e.target.value})}
placeholder="Observações sobre a devolução (opcional)"
/>
</div>
</div>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-outline"
onClick={() => setShowDevolucaoModal(false)}
>
Cancelar
</button>
<button type="submit" className="btn btn-success">
Registrar Devolução
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default Emprestimos;

View File

@@ -0,0 +1,338 @@
import React, { useState, useEffect } from 'react';
import {
FiPlus,
FiEdit,
FiTrash2,
FiSearch,
FiTruck,
FiMail,
FiPhone,
FiMapPin,
FiMessageSquare
} from 'react-icons/fi';
import { fornecedoresAPI } from '../services/api';
import ViewToggle from '../components/ViewToggle';
import toast from 'react-hot-toast';
const Fornecedores = () => {
const [fornecedores, setFornecedores] = useState([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [viewMode, setViewMode] = useState('list');
const [showModal, setShowModal] = useState(false);
const [editingSupplier, setEditingSupplier] = useState(null);
const [formData, setFormData] = useState({
razao_social: '',
telefone: '',
whatsapp: '',
endereco: '',
email: ''
});
useEffect(() => {
carregarFornecedores();
}, []);
const carregarFornecedores = async () => {
try {
setLoading(true);
const response = await fornecedoresAPI.listar();
setFornecedores(response.data);
} catch (error) {
console.error('Erro ao carregar fornecedores:', error);
toast.error('Erro ao carregar fornecedores');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (editingSupplier) {
await fornecedoresAPI.atualizar(editingSupplier.id, formData);
toast.success('Fornecedor atualizado com sucesso!');
} else {
await fornecedoresAPI.criar(formData);
toast.success('Fornecedor cadastrado com sucesso!');
}
setShowModal(false);
setEditingSupplier(null);
setFormData({
razao_social: '',
telefone: '',
whatsapp: '',
endereco: '',
email: ''
});
carregarFornecedores();
} catch (error) {
console.error('Erro ao salvar fornecedor:', error);
toast.error('Erro ao salvar fornecedor');
}
};
const handleEdit = (fornecedor) => {
setEditingSupplier(fornecedor);
setFormData({
razao_social: fornecedor.razao_social,
telefone: fornecedor.telefone || '',
whatsapp: fornecedor.whatsapp || '',
endereco: fornecedor.endereco || '',
email: fornecedor.email || ''
});
setShowModal(true);
};
const handleDelete = async (id) => {
if (window.confirm('Tem certeza que deseja excluir este fornecedor?')) {
try {
await fornecedoresAPI.deletar(id);
toast.success('Fornecedor excluído com sucesso!');
carregarFornecedores();
} catch (error) {
console.error('Erro ao excluir fornecedor:', error);
toast.error('Erro ao excluir fornecedor');
}
}
};
const filteredFornecedores = fornecedores.filter(fornecedor =>
fornecedor.razao_social.toLowerCase().includes(searchTerm.toLowerCase()) ||
(fornecedor.email && fornecedor.email.toLowerCase().includes(searchTerm.toLowerCase())) ||
(fornecedor.telefone && fornecedor.telefone.includes(searchTerm))
);
if (loading) {
return (
<div className="loading">
<div>Carregando fornecedores...</div>
</div>
);
}
return (
<div className="fornecedores fade-in">
<div className="page-header">
<div>
<h1>Fornecedores</h1>
<p>Gerencie os fornecedores da loja</p>
</div>
<div className="header-actions">
<ViewToggle
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Novo Fornecedor
</button>
</div>
</div>
{/* Barra de Pesquisa */}
<div className="search-box">
<FiSearch className="search-icon" />
<input
type="text"
placeholder="Pesquisar fornecedores..."
className="search-input"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Lista de Fornecedores */}
{filteredFornecedores.length === 0 ? (
<div className="empty-state">
<FiTruck size={48} />
<h3>Nenhum fornecedor encontrado</h3>
<p>Comece adicionando seu primeiro fornecedor</p>
<button
className="btn btn-primary"
onClick={() => setShowModal(true)}
>
<FiPlus />
Adicionar Fornecedor
</button>
</div>
) : (
<div className="suppliers-grid">
{filteredFornecedores.map((fornecedor) => (
<div key={fornecedor.id} className="supplier-card">
<div className="supplier-header">
<div className="supplier-avatar">
<FiTruck />
</div>
<div className="supplier-actions">
<button
className="btn-icon"
onClick={() => handleEdit(fornecedor)}
title="Editar"
>
<FiEdit />
</button>
<button
className="btn-icon btn-danger"
onClick={() => handleDelete(fornecedor.id)}
title="Excluir"
>
<FiTrash2 />
</button>
</div>
</div>
<div className="supplier-info">
<h3 className="supplier-name">{fornecedor.razao_social}</h3>
{fornecedor.email && (
<div className="supplier-detail">
<FiMail className="detail-icon" />
<span>{fornecedor.email}</span>
</div>
)}
{fornecedor.telefone && (
<div className="supplier-detail">
<FiPhone className="detail-icon" />
<span>{fornecedor.telefone}</span>
</div>
)}
{fornecedor.whatsapp && (
<div className="supplier-detail">
<FiMessageSquare className="detail-icon" />
<span>{fornecedor.whatsapp}</span>
</div>
)}
{fornecedor.endereco && (
<div className="supplier-detail">
<FiMapPin className="detail-icon" />
<span>{fornecedor.endereco}</span>
</div>
)}
<div className="supplier-meta">
<span className="supplier-date">
Cadastrado em {new Date(fornecedor.created_at).toLocaleDateString('pt-BR')}
</span>
</div>
</div>
</div>
))}
</div>
)}
{/* Modal de Fornecedor */}
{showModal && (
<div className="modal-overlay">
<div className="modal">
<div className="modal-header">
<h2 className="modal-title">
{editingSupplier ? 'Editar Fornecedor' : 'Novo Fornecedor'}
</h2>
<button
className="modal-close"
onClick={() => {
setShowModal(false);
setEditingSupplier(null);
setFormData({
razao_social: '',
telefone: '',
whatsapp: '',
endereco: '',
email: ''
});
}}
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label">Razão Social *</label>
<input
type="text"
className="form-input"
value={formData.razao_social}
onChange={(e) => setFormData({...formData, razao_social: e.target.value})}
required
/>
</div>
<div className="grid grid-2">
<div className="form-group">
<label className="form-label">Telefone</label>
<input
type="tel"
className="form-input"
value={formData.telefone}
onChange={(e) => setFormData({...formData, telefone: e.target.value})}
placeholder="(11) 3333-3333"
/>
</div>
<div className="form-group">
<label className="form-label">WhatsApp</label>
<input
type="tel"
className="form-input"
value={formData.whatsapp}
onChange={(e) => setFormData({...formData, whatsapp: e.target.value})}
placeholder="(11) 99999-9999"
/>
</div>
</div>
<div className="form-group">
<label className="form-label">E-mail</label>
<input
type="email"
className="form-input"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
/>
</div>
<div className="form-group">
<label className="form-label">Endereço</label>
<textarea
className="form-textarea"
value={formData.endereco}
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
placeholder="Rua, número, bairro, cidade, CEP"
rows="3"
/>
</div>
<div className="modal-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowModal(false);
setEditingSupplier(null);
}}
>
Cancelar
</button>
<button type="submit" className="btn btn-primary">
{editingSupplier ? 'Atualizar' : 'Cadastrar'} Fornecedor
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default Fornecedores;

1283
client/src/pages/Produtos.js Normal file

File diff suppressed because it is too large Load Diff

1471
client/src/pages/Vendas.js Normal file

File diff suppressed because it is too large Load Diff

110
client/src/services/api.js Normal file
View File

@@ -0,0 +1,110 @@
import axios from 'axios';
const API_BASE_URL = process.env.NODE_ENV === 'production'
? ''
: 'http://localhost:5000';
const api = axios.create({
baseURL: `${API_BASE_URL}/api`,
headers: {
'Content-Type': 'application/json',
},
});
// Interceptor para tratamento de erros
api.interceptors.response.use(
(response) => response,
(error) => {
console.error('Erro na API:', error);
return Promise.reject(error);
}
);
// === PRODUTOS ===
export const produtosAPI = {
listar: () => api.get('/produtos'),
criar: (produto) => api.post('/produtos', produto),
criarComFoto: (formData) => {
return api.post('/produtos', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
buscarPorId: (id) => api.get(`/produtos/${id}`),
atualizar: (id, produto) => api.put(`/produtos/${id}`, produto),
atualizarComFoto: (id, formData) => {
return api.put(`/produtos/${id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
deletar: (id) => api.delete(`/produtos/${id}`),
// Variações
listarVariacoes: (produtoId) => api.get(`/produtos/${produtoId}/variacoes`),
adicionarVariacao: (produtoId, formData) => {
return api.post(`/produtos/${produtoId}/variacoes`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
},
atualizarVariacao: (produtoId, variacaoId, dados) =>
api.put(`/produtos/${produtoId}/variacoes/${variacaoId}`, dados),
deletarVariacao: (produtoId, variacaoId) =>
api.delete(`/produtos/${produtoId}/variacoes/${variacaoId}`),
};
// === CLIENTES ===
export const clientesAPI = {
listar: () => api.get('/clientes'),
criar: (cliente) => api.post('/clientes', cliente),
buscarPorId: (id) => api.get(`/clientes/${id}`),
atualizar: (id, cliente) => api.put(`/clientes/${id}`, cliente),
deletar: (id) => api.delete(`/clientes/${id}`),
};
// === FORNECEDORES ===
export const fornecedoresAPI = {
listar: () => api.get('/fornecedores'),
criar: (fornecedor) => api.post('/fornecedores', fornecedor),
buscarPorId: (id) => api.get(`/fornecedores/${id}`),
atualizar: (id, fornecedor) => api.put(`/fornecedores/${id}`, fornecedor),
deletar: (id) => api.delete(`/fornecedores/${id}`),
};
// === DESPESAS ===
export const despesasAPI = {
listar: () => api.get('/despesas'),
criar: (despesa) => api.post('/despesas', despesa),
buscarPorId: (id) => api.get(`/despesas/${id}`),
atualizar: (id, despesa) => api.put(`/despesas/${id}`, despesa),
deletar: (id) => api.delete(`/despesas/${id}`),
// Tipos de despesas
listarTipos: () => api.get('/tipos-despesas'),
criarTipo: (tipo) => api.post('/tipos-despesas', tipo),
deletarTipo: (id) => api.delete(`/tipos-despesas/${id}`),
};
// === VENDAS ===
export const vendasAPI = {
listar: () => api.get('/vendas'),
criar: (venda) => api.post('/vendas', venda),
buscarPorId: (id) => api.get(`/vendas/${id}`),
atualizar: (id, venda) => api.put(`/vendas/${id}`, venda),
deletar: (id) => api.delete(`/vendas/${id}`),
// Itens da venda
adicionarItem: (vendaId, item) => api.post(`/vendas/${vendaId}/itens`, item),
removerItem: (vendaId, itemId) => api.delete(`/vendas/${vendaId}/itens/${itemId}`),
};
// === DASHBOARD ===
export const dashboardAPI = {
obterEstatisticas: () => api.get('/dashboard'),
};
export default api;

View File

@@ -0,0 +1,551 @@
/* Dashboard Contabilidade Completa */
.dashboard {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
.dashboard-header {
margin-bottom: 2rem;
}
.dashboard-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.dashboard-header p {
color: #6b7280;
font-size: 1.1rem;
}
.dashboard-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #6b7280;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* CONTABILIDADE COMPLETA */
.contabilidade-section {
margin-bottom: 3rem;
}
.contabilidade-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.contabilidade-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.contabilidade-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border-left: 4px solid;
transition: transform 0.2s, box-shadow 0.2s;
}
.contabilidade-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1);
}
.contabilidade-card.receita {
border-left-color: #10b981;
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
}
.contabilidade-card.custo {
border-left-color: #f59e0b;
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
}
.contabilidade-card.despesa {
border-left-color: #ef4444;
background: linear-gradient(135deg, #ffffff 0%, #fef2f2 100%);
}
.contabilidade-card.lucro {
border-left-color: #3b82f6;
background: linear-gradient(135deg, #ffffff 0%, #eff6ff 100%);
}
.card-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.card-icon {
font-size: 1.5rem;
color: inherit;
}
.receita .card-icon { color: #10b981; }
.custo .card-icon { color: #f59e0b; }
.despesa .card-icon { color: #ef4444; }
.lucro .card-icon { color: #3b82f6; }
.card-header span {
font-weight: 600;
color: #374151;
font-size: 1rem;
}
.card-value {
font-size: 2rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.card-subtitle {
color: #6b7280;
font-size: 0.9rem;
}
/* RESUMO FINANCEIRO */
.resumo-section {
margin-bottom: 3rem;
}
.resumo-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
}
.resumo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.resumo-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.resumo-card:hover {
transform: translateY(-2px);
}
.resumo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.resumo-header span {
font-weight: 600;
color: #374151;
}
.resumo-icon {
font-size: 1.5rem;
}
.resumo-icon.success { color: #10b981; }
.resumo-icon.danger { color: #ef4444; }
.resumo-icon.primary { color: #3b82f6; }
.resumo-value {
font-size: 1.8rem;
font-weight: 700;
}
.resumo-value.success { color: #10b981; }
.resumo-value.danger { color: #ef4444; }
.resumo-value.primary { color: #3b82f6; }
/* VENDAS A PRAZO */
.vendas-prazo-section {
margin-bottom: 3rem;
}
.vendas-prazo-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
}
.vendas-prazo-container {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.vendas-prazo-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.prazo-summary {
display: flex;
justify-content: space-between;
align-items: center;
}
.prazo-count {
color: #6b7280;
font-weight: 500;
}
.prazo-total {
font-size: 1.5rem;
font-weight: 700;
color: #3b82f6;
}
.vendas-prazo-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.venda-prazo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: background-color 0.2s;
}
.venda-prazo-item:hover {
background: #f3f4f6;
}
.venda-info {
flex: 1;
}
.cliente-nome {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.venda-valor {
color: #10b981;
font-weight: 600;
}
.venda-vencimento {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6b7280;
margin: 0 1rem;
}
.vencimento-icon {
font-size: 1rem;
}
.venda-actions {
display: flex;
gap: 0.5rem;
}
.btn-whatsapp {
background: #25d366;
color: white;
border: none;
border-radius: 6px;
padding: 0.5rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-whatsapp:hover {
background: #22c55e;
}
/* PARCELAS PENDENTES */
.parcelas-section {
margin-bottom: 3rem;
}
.parcelas-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
}
.parcelas-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.parcela-item {
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.parcela-info {
flex: 1;
}
.parcela-cliente {
font-weight: 600;
color: #1f2937;
margin-bottom: 0.25rem;
}
.parcela-valor {
color: #f59e0b;
font-weight: 600;
}
.parcela-detalhes {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
color: #6b7280;
font-size: 0.9rem;
}
/* EMPRÉSTIMOS */
.emprestimos-section {
margin-bottom: 3rem;
}
.emprestimos-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
}
.emprestimos-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.emprestimo-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border-left: 4px solid;
}
.emprestimo-card.aberto {
border-left-color: #f59e0b;
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
}
.emprestimo-card.quitado {
border-left-color: #10b981;
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
}
.emprestimo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.emprestimo-icon {
font-size: 1.5rem;
}
.aberto .emprestimo-icon { color: #f59e0b; }
.quitado .emprestimo-icon { color: #10b981; }
.emprestimo-header span {
font-weight: 600;
color: #374151;
}
.emprestimo-value {
font-size: 1.8rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.5rem;
}
.emprestimo-count {
color: #6b7280;
font-size: 0.9rem;
}
/* ESTATÍSTICAS GERAIS */
.estatisticas-section {
margin-bottom: 3rem;
}
.estatisticas-section h2 {
font-size: 1.8rem;
font-weight: 600;
color: #1f2937;
margin-bottom: 1.5rem;
}
.estatisticas-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon {
font-size: 2rem;
color: #3b82f6;
background: #eff6ff;
padding: 0.75rem;
border-radius: 8px;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 0.25rem;
}
.stat-label {
color: #6b7280;
font-size: 0.9rem;
font-weight: 500;
}
/* EMPTY STATE */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: #6b7280;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
color: #d1d5db;
}
.empty-state p {
font-size: 1.1rem;
font-weight: 500;
}
/* RESPONSIVIDADE */
@media (max-width: 768px) {
.dashboard {
padding: 1rem;
}
.dashboard-header h1 {
font-size: 2rem;
}
.contabilidade-grid,
.resumo-grid,
.emprestimos-grid,
.estatisticas-grid {
grid-template-columns: 1fr;
}
.venda-prazo-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.venda-vencimento {
margin: 0;
}
.venda-actions {
align-self: flex-end;
}
.parcela-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.parcela-detalhes {
align-items: flex-start;
}
}

View File

@@ -0,0 +1,404 @@
/* Dashboard Simples e Limpo */
.dashboard-simples {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
background: #f8fafc;
min-height: 100vh;
}
.dashboard-header {
margin-bottom: 2rem;
text-align: center;
}
.dashboard-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.5rem;
}
.dashboard-header p {
color: #64748b;
font-size: 1.1rem;
}
.dashboard-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #64748b;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* RESUMO FINANCEIRO PRINCIPAL */
.resumo-principal {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
}
.card-principal {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 1.5rem;
transition: transform 0.2s, box-shadow 0.2s;
border-left: 5px solid;
}
.card-principal:hover {
transform: translateY(-4px);
box-shadow: 0 12px 20px -4px rgba(0, 0, 0, 0.15);
}
.card-principal.receitas {
border-left-color: #10b981;
}
.card-principal.despesas {
border-left-color: #ef4444;
}
.card-principal.lucro {
border-left-color: #3b82f6;
}
.card-icon {
background: #f1f5f9;
border-radius: 12px;
padding: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.receitas .card-icon {
background: #ecfdf5;
color: #10b981;
}
.despesas .card-icon {
background: #fef2f2;
color: #ef4444;
}
.lucro .card-icon {
background: #eff6ff;
color: #3b82f6;
}
.card-icon svg {
font-size: 2rem;
}
.card-content h3 {
font-size: 1rem;
font-weight: 600;
color: #64748b;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.valor-principal {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.25rem;
}
.subtexto {
color: #64748b;
font-size: 0.9rem;
}
/* SEÇÕES */
.secao-vendas-prazo,
.secao-parcelas {
background: white;
border-radius: 16px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.secao-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.secao-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.total-prazo,
.total-parcelas {
background: #f1f5f9;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
color: #3b82f6;
}
/* LISTA DE VENDAS A PRAZO */
.lista-vendas-prazo {
display: flex;
flex-direction: column;
gap: 1rem;
}
.item-venda-prazo {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
transition: all 0.2s;
}
.item-venda-prazo:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
.venda-info {
flex: 1;
}
.cliente {
font-weight: 600;
color: #1e293b;
margin-bottom: 0.25rem;
font-size: 1.1rem;
}
.valor {
color: #10b981;
font-weight: 600;
font-size: 1.1rem;
}
.venda-vencimento {
display: flex;
align-items: center;
gap: 0.5rem;
color: #64748b;
margin: 0 1.5rem;
font-weight: 500;
}
.btn-whatsapp-simples {
background: #25d366;
color: white;
border: none;
border-radius: 10px;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-whatsapp-simples:hover {
background: #22c55e;
transform: scale(1.05);
}
.btn-whatsapp-simples svg {
font-size: 1.2rem;
}
.mais-vendas {
text-align: center;
padding: 1rem;
color: #64748b;
font-style: italic;
background: #f8fafc;
border-radius: 8px;
border: 1px dashed #cbd5e1;
}
/* LISTA DE PARCELAS */
.lista-parcelas {
display: flex;
flex-direction: column;
gap: 1rem;
}
.item-parcela {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: #f8fafc;
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.parcela-info {
flex: 1;
}
.parcela-detalhes {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
color: #64748b;
font-size: 0.9rem;
}
/* ESTATÍSTICAS RÁPIDAS */
.estatisticas-rapidas {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-item {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 1rem;
transition: transform 0.2s;
}
.stat-item:hover {
transform: translateY(-2px);
}
.stat-icon {
font-size: 2rem;
color: #3b82f6;
background: #eff6ff;
padding: 0.75rem;
border-radius: 10px;
}
.stat-numero {
font-size: 2rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.25rem;
}
.stat-label {
color: #64748b;
font-size: 0.9rem;
font-weight: 500;
}
/* EMPTY STATE */
.empty-recebimentos {
background: white;
border-radius: 16px;
padding: 3rem;
text-align: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.empty-icon {
font-size: 4rem;
color: #cbd5e1;
margin-bottom: 1rem;
}
.empty-recebimentos h3 {
font-size: 1.5rem;
font-weight: 600;
color: #64748b;
margin-bottom: 0.5rem;
}
.empty-recebimentos p {
color: #94a3b8;
font-size: 1.1rem;
}
/* RESPONSIVIDADE */
@media (max-width: 768px) {
.dashboard-simples {
padding: 1rem;
}
.dashboard-header h1 {
font-size: 2rem;
}
.resumo-principal {
grid-template-columns: 1fr;
}
.card-principal {
padding: 1.5rem;
}
.valor-principal {
font-size: 2rem;
}
.item-venda-prazo,
.item-parcela {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.venda-vencimento {
margin: 0;
}
.btn-whatsapp-simples {
align-self: flex-end;
}
.parcela-detalhes {
align-items: flex-start;
}
.estatisticas-rapidas {
grid-template-columns: 1fr;
}
.secao-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
/* PIX Integration Styles */
.pix-modal {
max-width: 500px;
width: 90vw;
}
.pix-content {
text-align: center;
}
.qr-code-section {
margin-bottom: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.qr-code-image {
max-width: 200px;
height: auto;
border: 2px solid #ddd;
border-radius: 8px;
padding: 10px;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.pix-copy-section {
margin-bottom: 20px;
}
.pix-code-container {
display: flex;
gap: 10px;
margin-top: 10px;
}
.pix-code-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
background: #f8f9fa;
}
.btn-copy-pix {
background: #00d4aa;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-copy-pix:hover {
background: #00b894;
transform: translateY(-1px);
}
.pix-info {
background: #e8f5e8;
padding: 15px;
border-radius: 8px;
text-align: left;
border-left: 4px solid #00d4aa;
}
.pix-instructions {
margin-top: 10px;
font-size: 14px;
color: #666;
line-height: 1.5;
}
.btn-pix {
background: #00d4aa;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-pix:hover {
background: #00b894;
transform: translateY(-1px);
}
.btn-pix:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.pix-status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-weight: bold;
}
.pix-status.pending {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.pix-status.approved {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.pix-status.rejected {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.pix-timer {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
text-align: center;
font-weight: bold;
color: #856404;
}
@media (max-width: 768px) {
.pix-modal {
width: 95vw;
margin: 10px;
}
.qr-code-image {
max-width: 150px;
}
.pix-code-container {
flex-direction: column;
}
.btn-copy-pix {
width: 100%;
}
}

View File

@@ -0,0 +1,595 @@
/* Estilos específicos para melhorias nas vendas */
/* Botões WhatsApp */
.whatsapp-buttons {
display: flex;
align-items: center;
}
.btn-outline-success {
background: transparent;
border: 1px solid #28a745;
color: #28a745;
transition: all 0.2s ease;
}
.btn-outline-success:hover {
background: #28a745;
color: white;
}
/* Container das parcelas */
.parcelas-container {
max-height: 300px;
overflow-y: auto;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.parcela-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-radius: 6px;
border: 1px solid #dee2e6;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.parcela-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.parcela-numero {
font-weight: 600;
color: #495057;
font-size: 14px;
}
.parcela-valor {
font-size: 16px;
font-weight: 700;
color: #28a745;
}
.parcela-data {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 150px;
}
.parcela-data .form-label {
font-size: 12px;
margin-bottom: 2px;
color: #6c757d;
}
.form-input-sm {
padding: 6px 8px;
font-size: 13px;
height: auto;
}
/* Responsividade para parcelas */
@media (max-width: 768px) {
.parcela-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.parcela-data {
min-width: auto;
width: 100%;
}
.parcela-data input {
width: 100%;
}
}
/* Estilos para coluna de produtos na tabela de vendas */
.sale-products {
max-width: 250px;
}
.products-summary {
display: flex;
flex-direction: column;
gap: 8px;
}
.product-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #007bff;
}
.product-name {
font-weight: 600;
color: #495057;
font-size: 13px;
line-height: 1.2;
}
.product-variation {
font-size: 12px;
color: #6c757d;
font-weight: 500;
}
.product-qty {
font-size: 11px;
color: #28a745;
font-weight: 600;
}
.more-products {
font-size: 12px;
color: #6c757d;
font-style: italic;
text-align: center;
padding: 4px;
background: #e9ecef;
border-radius: 4px;
}
.no-products {
color: #6c757d;
font-style: italic;
font-size: 12px;
}
/* Responsividade para tabela de vendas */
@media (max-width: 1200px) {
.sale-products {
max-width: 200px;
}
.product-name {
font-size: 12px;
}
.product-variation {
font-size: 11px;
}
}
@media (max-width: 768px) {
.sale-products {
max-width: 150px;
}
.product-item {
padding: 6px;
}
.product-name {
font-size: 11px;
}
.product-variation,
.product-qty {
font-size: 10px;
}
}
/* Melhorias nos totais */
.sale-totals .total-final {
background: #e8f5e8;
border-color: #28a745;
font-weight: 700;
font-size: 18px;
color: #155724;
}
/* Indicador de valor automático */
.valor-automatico {
position: relative;
}
.valor-automatico::after {
content: "Auto";
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: #007bff;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
font-weight: 600;
}
/* Seção de informações de pagamento */
.payment-section {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
border-left: 4px solid #007bff;
margin: 16px 0;
}
.payment-section h4 {
margin: 0 0 12px 0;
color: #495057;
font-size: 16px;
font-weight: 600;
}
.payment-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.payment-info-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #dee2e6;
}
.payment-info-item:last-child {
border-bottom: none;
}
.payment-label {
font-weight: 500;
color: #6c757d;
}
.payment-value {
font-weight: 600;
color: #495057;
}
/* Destaque para compra a prazo */
.prazo-highlight {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
border: 1px solid #ffc107;
padding: 12px;
border-radius: 6px;
margin: 8px 0;
}
.prazo-highlight .form-label {
color: #856404;
font-weight: 600;
}
/* Animações */
.parcela-item {
transition: all 0.2s ease;
}
.parcela-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Estados de validação */
.form-input.error {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-input.success {
border-color: #28a745;
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
}
/* Botões de ação adicionais */
.btn-icon.btn-info {
background: #17a2b8;
color: white;
}
.btn-icon.btn-info:hover {
background: #138496;
}
.btn-icon.btn-success {
background: #28a745;
color: white;
}
.btn-icon.btn-success:hover {
background: #218838;
}
/* ID da venda */
.sale-id {
font-family: 'Courier New', monospace;
font-size: 12px;
color: #6c757d;
background: #e9ecef;
font-size: 14px;
}
.item-quantity {
color: #495057;
font-weight: 500;
}
.item-price {
color: #28a745;
font-weight: 500;
}
/* Estilos para devoluções/trocas detalhadas */
.devolucoes-view {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.devolucao-item-completa {
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-radius: 12px;
border: 2px solid #e9ecef;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.devolucao-header-completa {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.operacao-info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.devolucao-data {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
.produto-devolvido {
margin: 1rem 0;
}
.produto-devolvido h4 {
color: #dc3545;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.produto-info-card {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 1rem;
margin-top: 0.5rem;
}
.produto-nome-dev {
font-weight: 600;
font-size: 1.1rem;
color: #333;
margin-bottom: 0.5rem;
}
.produto-detalhes-dev {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.produto-detalhes-dev span {
background: #fff;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
border: 1px solid #e9ecef;
}
.valores-dev {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.valores-dev span {
background: #d4edda;
color: #155724;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-weight: 500;
border: 1px solid #c3e6cb;
}
.motivo-operacao {
margin: 1rem 0;
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 1rem;
}
.motivo-operacao h4 {
color: #0066cc;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.motivo-texto {
background: #fff;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid #ccc;
font-style: italic;
color: #333;
}
.info-troca {
margin-top: 1rem;
}
.troca-aviso {
background: #d1ecf1;
color: #0c5460;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid #bee5eb;
font-size: 0.9rem;
}
/* Estilos para itens com status */
.item-header-status {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.item-status-badge {
flex-shrink: 0;
}
.item-devolvido {
background: #fff5f5 !important;
border-left: 4px solid #dc3545 !important;
}
.item-normal {
background: #f0fff4 !important;
border-left: 4px solid #28a745 !important;
}
.item-evento {
margin-top: 0.75rem;
padding: 0.75rem;
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border: 1px solid #bbdefb;
border-radius: 8px;
font-size: 0.9rem;
}
.evento-info {
color: #1565c0;
font-weight: 500;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
}
.evento-hora {
color: #666;
font-weight: 400;
font-size: 0.85rem;
}
.evento-observacao {
color: #666;
font-size: 0.8rem;
font-style: italic;
}
.item-observacao {
margin-top: 0.5rem;
padding: 0.5rem;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 4px;
font-size: 0.85rem;
color: #856404;
}
.badge-danger {
background-color: #dc3545;
color: white;
}
.badge-success {
background-color: #28a745;
color: white;
}
/* Badge para status com troca */
.badge-warning {
background-color: #ffc107;
color: #212529;
}
/* Modal maior para acomodar mais informações */
.modal-lg {
max-width: 900px !important;
width: 95% !important;
max-height: 90vh !important;
overflow-y: auto !important;
}
/* Scroll para itens quando há muitos */
.items-view {
max-height: 400px !important;
overflow-y: auto !important;
}
/* Modal de WhatsApp */
.whatsapp-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.whatsapp-info {
background: #e8f5e8;
padding: 12px;
border-radius: 6px;
border: 1px solid #28a745;
}
.whatsapp-info p {
margin: 4px 0;
color: #155724;
}
/* Responsividade */
@media (max-width: 768px) {
.info-grid {
grid-template-columns: 1fr;
}
.item-view {
flex-direction: column;
text-align: center;
}
.item-photo {
align-self: center;
}
.table-actions {
flex-wrap: wrap;
gap: 4px;
}
.btn-icon {
min-width: 32px;
min-height: 32px;
}
}