chore: sincroniza projeto para gitea
This commit is contained in:
124
client/package-lock.json
generated
124
client/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "liberi-kids-client",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.75.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
@@ -3991,6 +3992,123 @@
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
|
||||
"integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
|
||||
"integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@supabase/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
|
||||
"integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
|
||||
"integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js/node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
|
||||
"integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.75.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
|
||||
"integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.75.0",
|
||||
"@supabase/functions-js": "2.75.0",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "2.75.0",
|
||||
"@supabase/realtime-js": "2.75.0",
|
||||
"@supabase/storage-js": "2.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@surma/rollup-plugin-off-main-thread": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
||||
@@ -4707,6 +4825,12 @@
|
||||
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prettier": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.75.0",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"axios": "^1.5.0",
|
||||
"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",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"recharts": "^2.8.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -994,9 +994,11 @@
|
||||
}
|
||||
|
||||
.config-section {
|
||||
background: white;
|
||||
background-color: #ffffff;
|
||||
background-image: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1005,14 +1007,17 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease;
|
||||
padding: 24px;
|
||||
background-color: #ffffff;
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.config-header:hover {
|
||||
background-color: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
border-radius: 0;
|
||||
padding: 24px;
|
||||
margin: 0;
|
||||
box-shadow: inset 0 -1px 0 rgba(226, 232, 240, 0.8);
|
||||
}
|
||||
|
||||
.config-controls {
|
||||
@@ -1120,6 +1125,15 @@
|
||||
|
||||
.config-form {
|
||||
padding: 24px;
|
||||
background-color: #ffffff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.config-info-text {
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
@@ -2080,7 +2094,7 @@
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
GOOGLE SHEETS STYLES
|
||||
ESTILOS DA PÁGINA DE CONFIGURAÇÕES
|
||||
===================================================== */
|
||||
|
||||
.config-status {
|
||||
@@ -2224,7 +2238,7 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Responsivo para Google Sheets */
|
||||
/* Responsivo para seções de exportação */
|
||||
@media (max-width: 768px) {
|
||||
.export-buttons {
|
||||
flex-direction: column;
|
||||
@@ -2491,9 +2505,10 @@
|
||||
.image-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: 800px;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
width: 900px;
|
||||
height: 700px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -2534,8 +2549,8 @@
|
||||
.image-modal-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
@@ -2545,7 +2560,8 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f3f4f6;
|
||||
min-height: 400px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
@@ -2587,9 +2603,14 @@
|
||||
}
|
||||
|
||||
.image-details {
|
||||
width: 280px;
|
||||
padding: 20px;
|
||||
background: #f9fafb;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
border-left: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.image-details h4 {
|
||||
@@ -2654,6 +2675,161 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Operações agrupadas nas vendas */
|
||||
.operacoes-badge {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.operacoes-badge .badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.agrupamento-info {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agrupamento-info .info-text {
|
||||
margin: 0;
|
||||
color: #1565c0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.operacoes-agrupadas-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.operacao-agrupada-item {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.operacao-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.operacao-tipo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operacao-id {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
background: #e9ecef;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.operacao-data {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.operacao-detalhes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.operacao-detalhes > div {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.itens-operacao h5 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.itens-lista {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item-operacao {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.item-nome {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-detalhes {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.item-detalhes span {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Seções de configuração de alertas */
|
||||
.alert-config-section {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.alert-config-section h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-help-section {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-help-section .form-help {
|
||||
margin: 0;
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.form-help-section code {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.description-input-group {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -9,8 +9,10 @@ import Clientes from './pages/Clientes';
|
||||
import Fornecedores from './pages/Fornecedores';
|
||||
import Despesas from './pages/Despesas';
|
||||
import Vendas from './pages/Vendas';
|
||||
import PedidosCatalogo from './pages/PedidosCatalogo';
|
||||
import Devolucoes from './pages/Devolucoes';
|
||||
import Emprestimos from './pages/Emprestimos';
|
||||
import SiteCatalogo from './pages/SiteCatalogo';
|
||||
import Configuracoes from './pages/Configuracoes';
|
||||
import './App.css';
|
||||
|
||||
@@ -37,8 +39,10 @@ function App() {
|
||||
<Route path="/fornecedores" element={<Fornecedores />} />
|
||||
<Route path="/despesas" element={<Despesas />} />
|
||||
<Route path="/vendas" element={<Vendas />} />
|
||||
<Route path="/pedidos" element={<PedidosCatalogo />} />
|
||||
<Route path="/devolucoes" element={<Devolucoes />} />
|
||||
<Route path="/emprestimos" element={<Emprestimos />} />
|
||||
<Route path="/site/catalogo" element={<SiteCatalogo />} />
|
||||
<Route path="/configuracoes" element={<Configuracoes />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
|
||||
@@ -2,6 +2,61 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { FiX, FiSend, FiPhone, FiMessageCircle } from 'react-icons/fi';
|
||||
import './ChatWhatsApp.css';
|
||||
|
||||
const ZONA_HORARIA_BRASIL = 'America/Sao_Paulo';
|
||||
|
||||
const parseDate = (valor) => {
|
||||
if (!valor) return null;
|
||||
|
||||
if (valor instanceof Date && !isNaN(valor)) {
|
||||
return valor;
|
||||
}
|
||||
|
||||
if (typeof valor === 'number') {
|
||||
const date = new Date(valor);
|
||||
return isNaN(date) ? null : date;
|
||||
}
|
||||
|
||||
if (typeof valor === 'string') {
|
||||
const normalizada = valor.replace(' ', 'T');
|
||||
let date = new Date(normalizada);
|
||||
|
||||
if (!isNaN(date)) {
|
||||
return date;
|
||||
}
|
||||
|
||||
const partes = normalizada.split('-');
|
||||
if (partes.length === 3) {
|
||||
const [ano, mes, dia] = partes.map(Number);
|
||||
if (ano && mes && dia) {
|
||||
date = new Date(ano, mes - 1, dia);
|
||||
if (!isNaN(date)) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const formatarDataBrasil = (valor) => {
|
||||
const data = parseDate(valor);
|
||||
if (!data) return 'Data inválida';
|
||||
return new Intl.DateTimeFormat('pt-BR', {
|
||||
timeZone: ZONA_HORARIA_BRASIL
|
||||
}).format(data);
|
||||
};
|
||||
|
||||
const formatarHoraBrasil = (valor) => {
|
||||
const data = parseDate(valor);
|
||||
if (!data) return '--:--';
|
||||
return new Intl.DateTimeFormat('pt-BR', {
|
||||
timeZone: ZONA_HORARIA_BRASIL,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(data);
|
||||
};
|
||||
|
||||
const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
|
||||
const [mensagens, setMensagens] = useState([]);
|
||||
const [novaMensagem, setNovaMensagem] = useState('');
|
||||
@@ -102,16 +157,6 @@ const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -154,13 +199,13 @@ const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
|
||||
<>
|
||||
{mensagens.map((mensagem, index) => {
|
||||
const showDate = index === 0 ||
|
||||
formatarData(mensagem.created_at) !== formatarData(mensagens[index - 1].created_at);
|
||||
formatarDataBrasil(mensagem.created_at) !== formatarDataBrasil(mensagens[index - 1].created_at);
|
||||
|
||||
return (
|
||||
<React.Fragment key={mensagem.id || index}>
|
||||
{showDate && (
|
||||
<div className="chat-date-divider">
|
||||
{formatarData(mensagem.created_at)}
|
||||
{formatarDataBrasil(mensagem.created_at)}
|
||||
</div>
|
||||
)}
|
||||
<div className={`chat-message ${mensagem.tipo}`}>
|
||||
@@ -168,7 +213,7 @@ const ChatWhatsApp = ({ isOpen, onClose, cliente }) => {
|
||||
<p>{mensagem.mensagem}</p>
|
||||
<div className="message-info">
|
||||
<span className="message-time">
|
||||
{formatarHora(mensagem.created_at)}
|
||||
{formatarHoraBrasil(mensagem.created_at)}
|
||||
</span>
|
||||
{mensagem.tipo === 'enviada' && (
|
||||
<span className={`message-status ${mensagem.status}`}>
|
||||
|
||||
@@ -183,13 +183,15 @@
|
||||
/* Desktop */
|
||||
@media (min-width: 1024px) {
|
||||
.sidebar {
|
||||
position: static;
|
||||
transform: translateX(0);
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
margin-left: 280px;
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
FiX,
|
||||
FiWifi,
|
||||
FiWifiOff,
|
||||
FiCreditCard
|
||||
FiCreditCard,
|
||||
FiGlobe,
|
||||
FiList
|
||||
} from 'react-icons/fi';
|
||||
import './Layout.css';
|
||||
|
||||
@@ -28,8 +30,10 @@ const Layout = ({ children }) => {
|
||||
{ path: '/fornecedores', icon: FiTruck, label: 'Fornecedores' },
|
||||
{ path: '/despesas', icon: FiDollarSign, label: 'Despesas' },
|
||||
{ path: '/vendas', icon: FiShoppingCart, label: 'Vendas' },
|
||||
{ path: '/pedidos', icon: FiList, label: 'Pedidos' },
|
||||
{ path: '/devolucoes', icon: FiRotateCcw, label: 'Devolução/Troca' },
|
||||
{ path: '/emprestimos', icon: FiCreditCard, label: 'Empréstimos' },
|
||||
{ path: '/site/catalogo', icon: FiGlobe, label: 'Site / Catalogo' },
|
||||
{ path: '/configuracoes', icon: FiSettings, label: 'Configurações' },
|
||||
];
|
||||
|
||||
@@ -107,6 +111,7 @@ const Layout = ({ children }) => {
|
||||
<div className="header-title">
|
||||
<h1>Sistema de Controle de Estoque</h1>
|
||||
<p>Liberi Kids - Moda Infantil</p>
|
||||
<p style={{ fontSize: '0.85em', opacity: 0.7, marginTop: '-8px' }}>v1.0.0</p>
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
|
||||
200
client/src/config/supabase.js
Normal file
200
client/src/config/supabase.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = 'https://ydhzylfnpqlxnzfcclla.supabase.co'
|
||||
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkaHp5bGZucHFseG56ZmNjbGxhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA1NDA1NjIsImV4cCI6MjA3NjExNjU2Mn0.gIHxyAYngqkJ8z2Gt5ESYmG605vhY_LGTQB7Cjp4ZTA'
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
// Configurações dos buckets
|
||||
export const STORAGE_BUCKETS = {
|
||||
PRODUTOS: 'produtos',
|
||||
CATALOGO: 'catalogo'
|
||||
}
|
||||
|
||||
// Função para fazer upload de imagem
|
||||
export const uploadImage = async (file, bucket, path) => {
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop()
|
||||
const fileName = `${Date.now()}_${Math.random().toString(36).substring(2)}.${fileExt}`
|
||||
const filePath = path ? `${path}/${fileName}` : fileName
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(filePath, file)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
// Retornar URL pública
|
||||
const { data: publicData } = supabase.storage
|
||||
.from(bucket)
|
||||
.getPublicUrl(filePath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: filePath,
|
||||
url: publicData.publicUrl
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para deletar imagem
|
||||
export const deleteImage = async (bucket, path) => {
|
||||
try {
|
||||
const { error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.remove([path])
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar imagem:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para obter URL pública
|
||||
export const getPublicUrl = (bucket, path) => {
|
||||
const { data } = supabase.storage
|
||||
.from(bucket)
|
||||
.getPublicUrl(path)
|
||||
|
||||
return data.publicUrl
|
||||
}
|
||||
|
||||
// Função para login por telefone (WhatsApp)
|
||||
export const loginWithPhone = async (phone, password) => {
|
||||
try {
|
||||
// Primeiro, verificar se o cliente existe
|
||||
const { data: cliente, error: clienteError } = await supabase
|
||||
.from('clientes')
|
||||
.select('*')
|
||||
.eq('whatsapp', phone)
|
||||
.single()
|
||||
|
||||
if (clienteError || !cliente) {
|
||||
throw new Error('Cliente não encontrado')
|
||||
}
|
||||
|
||||
// Para simplificar, vamos usar o ID do cliente como "senha"
|
||||
// Em produção, você deve implementar um sistema de autenticação mais robusto
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: `${phone}@catalogo.local`, // Email fictício baseado no telefone
|
||||
password: password || cliente.id // Usar ID como senha temporária
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: data.user,
|
||||
cliente: cliente
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro no login:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para registrar cliente
|
||||
export const registerClient = async (clienteData) => {
|
||||
try {
|
||||
// Inserir cliente na tabela
|
||||
const { data: cliente, error: clienteError } = await supabase
|
||||
.from('clientes')
|
||||
.insert([clienteData])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (clienteError) throw clienteError
|
||||
|
||||
// Criar usuário de autenticação
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email: `${clienteData.whatsapp}@catalogo.local`,
|
||||
password: cliente.id, // Usar ID como senha temporária
|
||||
options: {
|
||||
data: {
|
||||
cliente_id: cliente.id,
|
||||
nome: clienteData.nome_completo,
|
||||
whatsapp: clienteData.whatsapp
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (authError) throw authError
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cliente: cliente,
|
||||
user: authData.user
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro no registro:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para logout
|
||||
export const logout = async () => {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Erro no logout:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Função para obter usuário atual
|
||||
export const getCurrentUser = async () => {
|
||||
try {
|
||||
const { data: { user }, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
|
||||
if (user) {
|
||||
// Buscar dados completos do cliente
|
||||
const { data: cliente, error: clienteError } = await supabase
|
||||
.from('clientes')
|
||||
.select('*')
|
||||
.eq('id', user.user_metadata.cliente_id)
|
||||
.single()
|
||||
|
||||
if (clienteError) throw clienteError
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: user,
|
||||
cliente: cliente
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, user: null, cliente: null }
|
||||
} catch (error) {
|
||||
console.error('Erro ao obter usuário:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default supabase
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,26 @@ import { dashboardAPI, clientesAPI, despesasAPI, fornecedoresAPI } from '../serv
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const Dashboard = () => {
|
||||
|
||||
const enviarWhatsAppAutomatico = async ({ telefone, mensagem }) => {
|
||||
if (!telefone || !mensagem) return false;
|
||||
try {
|
||||
const response = await fetch('/api/chat/enviar', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ telefone, mensagem, clienteNome: 'Cliente' })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
console.error('Erro ao enviar WhatsApp automático:', data.error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar WhatsApp automático:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const [dashboardData, setDashboardData] = useState({
|
||||
contabilidade: {
|
||||
receitaBruta: 0,
|
||||
@@ -78,6 +98,8 @@ const Dashboard = () => {
|
||||
const [fornecedores, setFornecedores] = useState([]);
|
||||
const [tiposDespesas, setTiposDespesas] = useState([]);
|
||||
const [clientes, setClientes] = useState([]);
|
||||
const [vendasPrazo, setVendasPrazo] = useState([]);
|
||||
const [loadingVendasPrazo, setLoadingVendasPrazo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
carregarDashboard();
|
||||
@@ -123,23 +145,21 @@ const Dashboard = () => {
|
||||
|
||||
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
|
||||
})
|
||||
});
|
||||
const telefone = (venda?.cliente_whatsapp || venda?.cliente_telefone || '').replace(/\D/g, '');
|
||||
if (!telefone) {
|
||||
toast.error('Cliente sem telefone/whatsapp cadastrado');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const valorFinal = parseFloat(venda.valor_total) - parseFloat(venda.desconto || 0);
|
||||
const valorFormatado = valorFinal.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
const mensagem = `Olá ${venda.cliente_nome || ''}! Lembramos que sua parcela de ${valorFormatado} está próxima do vencimento. Entre em contato conosco se precisar de ajuda.`;
|
||||
|
||||
const enviado = await enviarWhatsAppAutomatico({ telefone, mensagem });
|
||||
if (enviado) {
|
||||
toast.success('Mensagem enviada com sucesso!');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast.error(error.message || 'Erro ao enviar mensagem');
|
||||
toast.error('Não foi possível enviar a mensagem.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar WhatsApp:', error);
|
||||
@@ -182,6 +202,31 @@ const Dashboard = () => {
|
||||
{ name: 'Meia Estação', value: 20, color: '#f093fb' },
|
||||
];
|
||||
|
||||
const contabilidade = dashboardData?.contabilidade || {};
|
||||
const resumoFinanceiro = dashboardData?.resumoFinanceiro || {};
|
||||
const emprestimosResumo = dashboardData?.emprestimos || {};
|
||||
const vendasPrazoResumo = dashboardData?.vendasPrazo || {};
|
||||
const parcelasResumo = dashboardData?.parcelasPendentes || {};
|
||||
const estatisticasResumo = dashboardData?.estatisticas || {};
|
||||
|
||||
const formatCurrency = (valor) =>
|
||||
Number(valor || 0).toLocaleString('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
});
|
||||
|
||||
const stats = {
|
||||
totalProdutos: { count: estatisticasResumo.totalProdutos || 0 },
|
||||
totalClientes: { count: estatisticasResumo.totalClientes || 0 },
|
||||
totalFornecedores: { count: estatisticasResumo.totalFornecedores || 0 },
|
||||
vendasMes: {
|
||||
total: resumoFinanceiro.receitasMes || 0,
|
||||
count: resumoFinanceiro.totalVendas || 0
|
||||
},
|
||||
estoqueTotal: { total: estatisticasResumo.estoqueTotal || 0 },
|
||||
despesasMes: { total: contabilidade.totalDespesas || 0 }
|
||||
};
|
||||
|
||||
const estatisticas = [
|
||||
{
|
||||
title: 'Total de Produtos',
|
||||
@@ -230,7 +275,7 @@ const Dashboard = () => {
|
||||
},
|
||||
{
|
||||
title: 'Faturamento Mensal',
|
||||
value: `R$ ${(stats.vendasMes?.total || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`,
|
||||
value: formatCurrency(resumoFinanceiro.receitasMes),
|
||||
icon: FiDollarSign,
|
||||
color: '#06b6d4',
|
||||
bgColor: '#ecfeff',
|
||||
@@ -262,19 +307,19 @@ const Dashboard = () => {
|
||||
<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 })}
|
||||
{formatCurrency(resumoFinanceiro.receitasMes)}
|
||||
</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 })}
|
||||
{formatCurrency(resumoFinanceiro.despesasMes || contabilidade.totalDespesas)}
|
||||
</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 })}
|
||||
{formatCurrency(resumoFinanceiro.lucroEstimado ?? contabilidade.lucroReal)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -297,6 +342,11 @@ const Dashboard = () => {
|
||||
</div>
|
||||
|
||||
<div className="vendas-prazo-content">
|
||||
<div className="vendas-prazo-stats">
|
||||
<span>Total pendente: {formatCurrency(vendasPrazoResumo.total)}</span>
|
||||
<span>Vendas: {vendasPrazoResumo.quantidade || 0}</span>
|
||||
<span>Parcelas pendentes: {parcelasResumo.quantidade || 0}</span>
|
||||
</div>
|
||||
{loadingVendasPrazo ? (
|
||||
<div className="loading-vendas">Carregando vendas...</div>
|
||||
) : vendasPrazo.length === 0 ? (
|
||||
@@ -349,6 +399,48 @@ const Dashboard = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="summary-card">
|
||||
<h3>Empréstimos</h3>
|
||||
<div className="summary-items">
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">Em aberto:</span>
|
||||
<span className="summary-value negative">
|
||||
{formatCurrency(emprestimosResumo.totalAberto)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">Quitado:</span>
|
||||
<span className="summary-value positive">
|
||||
{formatCurrency(emprestimosResumo.totalQuitado)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">Contratos:</span>
|
||||
<span className="summary-value">
|
||||
{emprestimosResumo.quantidade || 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="stats-grid">
|
||||
{estatisticas.map((item) => (
|
||||
<div key={item.title} className="stat-card" style={{ backgroundColor: item.bgColor }}>
|
||||
<div className="stat-icon" style={{ color: item.color }}>
|
||||
<item.icon size={22} />
|
||||
</div>
|
||||
<div className="stat-content">
|
||||
<span className="stat-title">{item.title}</span>
|
||||
<span className="stat-value">{item.value}</span>
|
||||
<span className={`stat-trend ${item.trendUp ? 'up' : 'down'}`}>
|
||||
{item.trend}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -22,8 +22,35 @@ const Fornecedores = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingSupplier, setEditingSupplier] = useState(null);
|
||||
|
||||
|
||||
const formatFornecedor = (fornecedor) => {
|
||||
if (!fornecedor) return {
|
||||
id: undefined,
|
||||
nome: 'Fornecedor sem nome',
|
||||
telefone: '',
|
||||
whatsapp: '',
|
||||
endereco: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
const nome = fornecedor.nome || fornecedor.razao_social || 'Fornecedor sem nome';
|
||||
const telefone = fornecedor.telefone || fornecedor.whatsapp || '';
|
||||
const whatsapp = fornecedor.whatsapp || fornecedor.telefone || '';
|
||||
|
||||
return {
|
||||
...fornecedor,
|
||||
nome,
|
||||
telefone,
|
||||
whatsapp,
|
||||
endereco: fornecedor.endereco || '',
|
||||
email: fornecedor.email || ''
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeSearch = (value) => (value || '').toString().toLowerCase();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
razao_social: '',
|
||||
nome: '',
|
||||
telefone: '',
|
||||
whatsapp: '',
|
||||
endereco: '',
|
||||
@@ -38,7 +65,8 @@ const Fornecedores = () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fornecedoresAPI.listar();
|
||||
setFornecedores(response.data);
|
||||
const dados = (response.data || []).map(formatFornecedor);
|
||||
setFornecedores(dados);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar fornecedores:', error);
|
||||
toast.error('Erro ao carregar fornecedores');
|
||||
@@ -61,7 +89,7 @@ const Fornecedores = () => {
|
||||
setShowModal(false);
|
||||
setEditingSupplier(null);
|
||||
setFormData({
|
||||
razao_social: '',
|
||||
nome: '',
|
||||
telefone: '',
|
||||
whatsapp: '',
|
||||
endereco: '',
|
||||
@@ -75,13 +103,14 @@ const Fornecedores = () => {
|
||||
};
|
||||
|
||||
const handleEdit = (fornecedor) => {
|
||||
setEditingSupplier(fornecedor);
|
||||
const fornecedorFormatado = formatFornecedor(fornecedor);
|
||||
setEditingSupplier(fornecedorFormatado);
|
||||
setFormData({
|
||||
razao_social: fornecedor.razao_social,
|
||||
telefone: fornecedor.telefone || '',
|
||||
whatsapp: fornecedor.whatsapp || '',
|
||||
endereco: fornecedor.endereco || '',
|
||||
email: fornecedor.email || ''
|
||||
nome: fornecedorFormatado.nome,
|
||||
telefone: fornecedorFormatado.telefone || '',
|
||||
whatsapp: fornecedorFormatado.whatsapp || '',
|
||||
endereco: fornecedorFormatado.endereco || '',
|
||||
email: fornecedorFormatado.email || ''
|
||||
});
|
||||
setShowModal(true);
|
||||
};
|
||||
@@ -99,11 +128,18 @@ const Fornecedores = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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))
|
||||
);
|
||||
const searchTermNormalized = normalizeSearch(searchTerm);
|
||||
|
||||
const filteredFornecedores = fornecedores.filter((fornecedor) => {
|
||||
if (!searchTermNormalized) return true;
|
||||
|
||||
return (
|
||||
normalizeSearch(fornecedor.nome).includes(searchTermNormalized) ||
|
||||
normalizeSearch(fornecedor.email).includes(searchTermNormalized) ||
|
||||
normalizeSearch(fornecedor.telefone).includes(searchTermNormalized) ||
|
||||
normalizeSearch(fornecedor.whatsapp).includes(searchTermNormalized)
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -188,7 +224,7 @@ const Fornecedores = () => {
|
||||
</div>
|
||||
|
||||
<div className="supplier-info">
|
||||
<h3 className="supplier-name">{fornecedor.razao_social}</h3>
|
||||
<h3 className="supplier-name">{fornecedor.nome}</h3>
|
||||
|
||||
{fornecedor.email && (
|
||||
<div className="supplier-detail">
|
||||
@@ -243,7 +279,7 @@ const Fornecedores = () => {
|
||||
setShowModal(false);
|
||||
setEditingSupplier(null);
|
||||
setFormData({
|
||||
razao_social: '',
|
||||
nome: '',
|
||||
telefone: '',
|
||||
whatsapp: '',
|
||||
endereco: '',
|
||||
@@ -257,12 +293,12 @@ const Fornecedores = () => {
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Razão Social *</label>
|
||||
<label className="form-label">Nome do Fornecedor *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
value={formData.razao_social}
|
||||
onChange={(e) => setFormData({...formData, razao_social: e.target.value})}
|
||||
value={formData.nome}
|
||||
onChange={(e) => setFormData({...formData, nome: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
155
client/src/pages/PedidosCatalogo.js
Normal file
155
client/src/pages/PedidosCatalogo.js
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
FiRefreshCw,
|
||||
FiClipboard,
|
||||
FiClock,
|
||||
FiPhone,
|
||||
FiMapPin,
|
||||
FiPackage
|
||||
} from 'react-icons/fi';
|
||||
import toast from 'react-hot-toast';
|
||||
import '../styles/pedidos-catalogo.css';
|
||||
|
||||
const formatarMoeda = (valor) => {
|
||||
return (Number(valor) || 0).toLocaleString('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
});
|
||||
};
|
||||
|
||||
const formatarData = (dataISO) => {
|
||||
if (!dataISO) return '-';
|
||||
try {
|
||||
return new Date(dataISO).toLocaleString('pt-BR', {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'short'
|
||||
});
|
||||
} catch (error) {
|
||||
return dataISO;
|
||||
}
|
||||
};
|
||||
|
||||
const PedidosCatalogo = () => {
|
||||
const [pedidos, setPedidos] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const carregarPedidos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch('/api/catalogo/pedidos');
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao carregar pedidos');
|
||||
}
|
||||
const data = await response.json();
|
||||
setPedidos(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar pedidos:', err);
|
||||
setError('Não foi possível carregar os pedidos.');
|
||||
toast.error('Erro ao carregar pedidos do catálogo.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
carregarPedidos();
|
||||
}, []);
|
||||
|
||||
const pedidosOrdenados = useMemo(() => {
|
||||
return [...pedidos].sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
|
||||
}, [pedidos]);
|
||||
|
||||
return (
|
||||
<div className="pedidos-catalogo-page fade-in">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1>Pedidos do Catálogo</h1>
|
||||
<p>Pedidos registrados automaticamente através do catálogo online</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={carregarPedidos}
|
||||
disabled={loading}
|
||||
>
|
||||
<FiRefreshCw />
|
||||
{loading ? 'Atualizando...' : 'Atualizar'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pedidosOrdenados.length === 0 && !loading ? (
|
||||
<div className="empty-state">
|
||||
<FiClipboard size={48} />
|
||||
<p>Nenhum pedido registrado ainda</p>
|
||||
<span>Assim que um cliente finalizar uma compra no catálogo, o pedido aparecerá aqui.</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pedidos-lista">
|
||||
{pedidosOrdenados.map((pedido) => (
|
||||
<div key={pedido.id} className="pedido-card">
|
||||
<header className="pedido-card-header">
|
||||
<div className="pedido-id">
|
||||
<FiClipboard />
|
||||
<span>{pedido.codigo || pedido.id || 'Pedido sem ID'}</span>
|
||||
</div>
|
||||
<div className="pedido-data">
|
||||
<FiClock />
|
||||
<span>{formatarData(pedido.createdAt)}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="pedido-cliente">
|
||||
<div className="cliente-nome">{pedido.cliente?.nome || 'Cliente não informado'}</div>
|
||||
<div className="cliente-info">
|
||||
<span><FiPhone /> {pedido.cliente?.whatsapp || 'Sem contato'}</span>
|
||||
{pedido.cliente?.endereco && (
|
||||
<span><FiMapPin /> {pedido.cliente.endereco}</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pedido-itens">
|
||||
<h4>Itens</h4>
|
||||
<div className="pedido-itens-lista">
|
||||
{(pedido.itens || []).map((item, index) => (
|
||||
<div key={`${pedido.id}-item-${index}`} className="pedido-item">
|
||||
<div className="pedido-item-info">
|
||||
<strong>{item.nome}</strong>
|
||||
<div className="pedido-item-meta">
|
||||
<span><FiPackage /> {item.codigo || 'Sem ID'}</span>
|
||||
<span>Tam: {item.tamanho || '-'}</span>
|
||||
<span>Cor: {item.cor || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pedido-item-valores">
|
||||
<span>{item.quantidade || 0} x {formatarMoeda(item.preco)}</span>
|
||||
<strong>{formatarMoeda(item.subtotal)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="pedido-footer">
|
||||
<div className="pedido-total">
|
||||
<span>Total do pedido</span>
|
||||
<strong>{formatarMoeda(pedido.total)}</strong>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PedidosCatalogo;
|
||||
@@ -14,6 +14,13 @@ import { produtosAPI, fornecedoresAPI } from '../services/api';
|
||||
import ViewToggle from '../components/ViewToggle';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const BASE_MEDIA_URL = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:5000';
|
||||
|
||||
const resolveImageUrl = (url) => {
|
||||
if (!url) return null;
|
||||
return url.startsWith('http') ? url : `${BASE_MEDIA_URL}${url}`;
|
||||
};
|
||||
|
||||
const Produtos = () => {
|
||||
const [produtos, setProdutos] = useState([]);
|
||||
const [fornecedores, setFornecedores] = useState([]);
|
||||
@@ -237,15 +244,7 @@ const Produtos = () => {
|
||||
try {
|
||||
console.log('Iniciando envio do produto...', formData);
|
||||
|
||||
// Verificar se Google Drive está configurado
|
||||
const googleDriveStatus = await fetch('/api/google-drive/status');
|
||||
const driveStatusData = await googleDriveStatus.json();
|
||||
const useGoogleDrive = driveStatusData.status === 'connected';
|
||||
|
||||
if (useGoogleDrive) {
|
||||
console.log('🔄 Usando Google Drive para armazenar fotos');
|
||||
toast.loading('Enviando fotos para Google Drive...', { id: 'upload-drive' });
|
||||
}
|
||||
// Usar armazenamento local
|
||||
|
||||
const formDataToSend = new FormData();
|
||||
formDataToSend.append('id_produto', formData.id_produto || '');
|
||||
@@ -257,7 +256,6 @@ const Produtos = () => {
|
||||
formDataToSend.append('fornecedor_id', formData.fornecedor_id || '');
|
||||
formDataToSend.append('valor_compra', formData.valor_compra);
|
||||
formDataToSend.append('valor_revenda', formData.valor_revenda);
|
||||
formDataToSend.append('use_google_drive', useGoogleDrive.toString());
|
||||
|
||||
// Adicionar dados das variações apenas para criação
|
||||
if (!editingProduct) {
|
||||
@@ -289,11 +287,7 @@ const Produtos = () => {
|
||||
const response = await produtosAPI.criarComFoto(formDataToSend);
|
||||
console.log('Resposta do servidor:', response);
|
||||
|
||||
if (useGoogleDrive) {
|
||||
toast.success('Produto criado e fotos salvas no Google Drive!', { id: 'upload-drive' });
|
||||
} else {
|
||||
toast.success('Produto criado com sucesso!');
|
||||
}
|
||||
toast.success('Produto criado com sucesso!');
|
||||
}
|
||||
|
||||
setShowModal(false);
|
||||
@@ -310,7 +304,7 @@ const Produtos = () => {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
toast.error(errorMessage, { id: 'upload-drive' });
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -376,7 +370,7 @@ const Produtos = () => {
|
||||
// Adicionar foto principal se existir
|
||||
if (produto.foto_principal_url) {
|
||||
images.push({
|
||||
url: produto.foto_principal_url,
|
||||
url: resolveImageUrl(produto.foto_principal_url),
|
||||
title: `${produto.marca} - ${produto.nome}`,
|
||||
type: 'principal'
|
||||
});
|
||||
@@ -384,10 +378,21 @@ const Produtos = () => {
|
||||
|
||||
// Adicionar fotos das variações
|
||||
if (produto.produto_variacoes && produto.produto_variacoes.length > 0) {
|
||||
produto.produto_variacoes.forEach((variacao, index) => {
|
||||
if (variacao.foto_url) {
|
||||
produto.produto_variacoes.forEach((variacao) => {
|
||||
const fotosVariacao = Array.isArray(variacao.fotos) ? variacao.fotos : [];
|
||||
|
||||
if (fotosVariacao.length > 0) {
|
||||
fotosVariacao.forEach((fotoUrl, index) => {
|
||||
images.push({
|
||||
url: resolveImageUrl(fotoUrl),
|
||||
title: `${produto.marca} - ${produto.nome} (${variacao.tamanho} - ${variacao.cor})` + (fotosVariacao.length > 1 ? ` - Foto ${index + 1}` : ''),
|
||||
type: 'variacao',
|
||||
variacao: variacao
|
||||
});
|
||||
});
|
||||
} else if (variacao.foto_url) {
|
||||
images.push({
|
||||
url: variacao.foto_url,
|
||||
url: resolveImageUrl(variacao.foto_url),
|
||||
title: `${produto.marca} - ${produto.nome} (${variacao.tamanho} - ${variacao.cor})`,
|
||||
type: 'variacao',
|
||||
variacao: variacao
|
||||
@@ -658,7 +663,7 @@ const Produtos = () => {
|
||||
>
|
||||
{produto.foto_principal_url || (produto.produto_variacoes && produto.produto_variacoes.find(v => v.foto_url)) ? (
|
||||
<img
|
||||
src={produto.foto_principal_url || produto.produto_variacoes.find(v => v.foto_url)?.foto_url}
|
||||
src={resolveImageUrl(produto.foto_principal_url || produto.produto_variacoes.find(v => v.foto_url)?.foto_url)}
|
||||
alt={produto.nome}
|
||||
className="product-thumbnail"
|
||||
onError={(e) => {
|
||||
@@ -845,7 +850,7 @@ const Produtos = () => {
|
||||
<option value="">Selecione um fornecedor</option>
|
||||
{fornecedores.map((fornecedor) => (
|
||||
<option key={fornecedor.id} value={fornecedor.id}>
|
||||
{fornecedor.razao_social}
|
||||
{fornecedor.nome}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -1142,7 +1147,7 @@ const Produtos = () => {
|
||||
{variacao.foto_url && (
|
||||
<div className="variacao-image">
|
||||
<img
|
||||
src={`http://localhost:5000${variacao.foto_url}`}
|
||||
src={resolveImageUrl(variacao.foto_url)}
|
||||
alt={`${variacao.cor} - ${variacao.tamanho}`}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
@@ -1226,34 +1231,43 @@ const Produtos = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedImages[currentImageIndex]?.type === 'variacao' && (
|
||||
<div className="image-details">
|
||||
<h4>Detalhes da Variação:</h4>
|
||||
<p><strong>Tamanho:</strong> {selectedImages[currentImageIndex].variacao.tamanho}</p>
|
||||
<p><strong>Cor:</strong> {selectedImages[currentImageIndex].variacao.cor}</p>
|
||||
<p><strong>Quantidade:</strong> {selectedImages[currentImageIndex].variacao.quantidade}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedImages[currentImageIndex]?.type === 'placeholder' && (
|
||||
<div className="image-details">
|
||||
<h4>📷 Sem Imagens</h4>
|
||||
<p>Este produto ainda não possui imagens cadastradas.</p>
|
||||
<p>Para adicionar imagens, edite o produto e faça upload das fotos nas variações.</p>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setShowImageModal(false);
|
||||
handleEdit(selectedProduct);
|
||||
}}
|
||||
>
|
||||
<FiEdit />
|
||||
Editar Produto e Adicionar Fotos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="image-details">
|
||||
{selectedImages[currentImageIndex]?.type === 'variacao' && (
|
||||
<>
|
||||
<h4>Detalhes da Variação:</h4>
|
||||
<p><strong>Tamanho:</strong> {selectedImages[currentImageIndex].variacao.tamanho}</p>
|
||||
<p><strong>Cor:</strong> {selectedImages[currentImageIndex].variacao.cor}</p>
|
||||
<p><strong>Quantidade:</strong> {selectedImages[currentImageIndex].variacao.quantidade}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedImages[currentImageIndex]?.type === 'placeholder' && (
|
||||
<>
|
||||
<h4>📷 Sem Imagens</h4>
|
||||
<p>Este produto ainda não possui imagens cadastradas.</p>
|
||||
<p>Para adicionar imagens, edite o produto e faça upload das fotos nas variações.</p>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setShowImageModal(false);
|
||||
handleEdit(selectedProduct);
|
||||
}}
|
||||
>
|
||||
<FiEdit />
|
||||
Editar Produto e Adicionar Fotos
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedImages[currentImageIndex]?.type === 'principal' && (
|
||||
<>
|
||||
<h4>Foto Principal</h4>
|
||||
<p>Esta é a imagem principal do produto.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedImages.length > 1 && (
|
||||
|
||||
671
client/src/pages/SiteCatalogo.js
Normal file
671
client/src/pages/SiteCatalogo.js
Normal file
@@ -0,0 +1,671 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
FiGlobe,
|
||||
FiPackage,
|
||||
FiImage,
|
||||
FiEye,
|
||||
FiEyeOff,
|
||||
FiRefreshCw,
|
||||
FiSave,
|
||||
FiSettings
|
||||
} from 'react-icons/fi';
|
||||
import toast from 'react-hot-toast';
|
||||
import '../styles/site-catalogo.css';
|
||||
import '../styles/site-catalogo-table.css';
|
||||
|
||||
const SiteCatalogo = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [produtos, setProdutos] = useState([]);
|
||||
const [precosPromocionais, setPrecosPromocionais] = useState({});
|
||||
const [precosOriginais, setPrecosOriginais] = useState({});
|
||||
const [precosAlterados, setPrecosAlterados] = useState({});
|
||||
const [precosSalvando, setPrecosSalvando] = useState({});
|
||||
const [catalogoConfig, setCatalogoConfig] = useState({
|
||||
catalogoAtivo: false,
|
||||
exibirPrecos: true,
|
||||
exibirEstoque: false,
|
||||
exibirNovidades: true,
|
||||
exibirPromocoes: true
|
||||
});
|
||||
const [modalFotosOpen, setModalFotosOpen] = useState(false);
|
||||
const [produtoSelecionado, setProdutoSelecionado] = useState(null);
|
||||
const [fotosAdicionais, setFotosAdicionais] = useState([]);
|
||||
const [uploadingPhoto, setUploadingPhoto] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
carregarProdutos();
|
||||
carregarConfiguracoes();
|
||||
}, []);
|
||||
|
||||
const carregarProdutos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/produtos');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProdutos(data);
|
||||
const precos = {};
|
||||
const originais = {};
|
||||
data.forEach((produto) => {
|
||||
const valor = produto.preco_promocional;
|
||||
const valorFormatado =
|
||||
valor === null || valor === undefined || valor === ''
|
||||
? ''
|
||||
: Number(valor).toFixed(2);
|
||||
precos[produto.id] = valorFormatado;
|
||||
originais[produto.id] = valorFormatado;
|
||||
});
|
||||
setPrecosPromocionais(precos);
|
||||
setPrecosOriginais(originais);
|
||||
setPrecosAlterados({});
|
||||
setPrecosSalvando({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar produtos:', error);
|
||||
toast.error('Erro ao carregar produtos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const carregarConfiguracoes = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/configuracoes/catalogo');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setCatalogoConfig(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar configurações:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProdutoPromocao = async (produtoId, emPromocao) => {
|
||||
try {
|
||||
const response = await fetch(`/api/produtos/${produtoId}/promocao`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ emPromocao: !emPromocao }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Promoção atualizada!');
|
||||
carregarProdutos();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar promoção:', error);
|
||||
toast.error('Erro ao atualizar promoção');
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProdutoNovidade = async (produtoId, novidade) => {
|
||||
try {
|
||||
const response = await fetch(`/api/produtos/${produtoId}/novidade`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ novidade: !novidade }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Novidade atualizada!');
|
||||
carregarProdutos();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar novidade:', error);
|
||||
toast.error('Erro ao atualizar novidade');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrecoPromocionalChange = (produtoId, valor) => {
|
||||
setPrecosPromocionais(prev => ({
|
||||
...prev,
|
||||
[produtoId]: valor
|
||||
}));
|
||||
setPrecosAlterados(prev => ({
|
||||
...prev,
|
||||
[produtoId]: valor !== (precosOriginais[produtoId] ?? '')
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePrecoPromocionalBlur = (produtoId) => {
|
||||
const valorAtual = precosPromocionais[produtoId];
|
||||
|
||||
if (valorAtual === '' || valorAtual === null || valorAtual === undefined) {
|
||||
setPrecosAlterados(prev => ({
|
||||
...prev,
|
||||
[produtoId]: (precosOriginais[produtoId] ?? '') !== ''
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const numero = Number(valorAtual);
|
||||
if (Number.isNaN(numero)) {
|
||||
toast.error('Informe um valor numérico válido');
|
||||
return;
|
||||
}
|
||||
|
||||
const formatado = numero.toFixed(2);
|
||||
|
||||
setPrecosPromocionais(prev => ({
|
||||
...prev,
|
||||
[produtoId]: formatado
|
||||
}));
|
||||
|
||||
setPrecosAlterados(prev => ({
|
||||
...prev,
|
||||
[produtoId]: formatado !== (precosOriginais[produtoId] ?? '')
|
||||
}));
|
||||
};
|
||||
|
||||
const salvarPrecoPromocional = async (produtoId) => {
|
||||
const valorAtual = precosPromocionais[produtoId];
|
||||
const precoFormatado = valorAtual === '' || valorAtual === null || valorAtual === undefined
|
||||
? ''
|
||||
: Number(valorAtual).toFixed(2);
|
||||
const precoPayload = valorAtual === '' || valorAtual === null || valorAtual === undefined
|
||||
? null
|
||||
: Number(valorAtual);
|
||||
|
||||
if (precoPayload !== null && (Number.isNaN(precoPayload) || precoPayload < 0)) {
|
||||
toast.error('Informe um valor válido para o desconto');
|
||||
return;
|
||||
}
|
||||
|
||||
setPrecosSalvando(prev => ({
|
||||
...prev,
|
||||
[produtoId]: true
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/produtos/${produtoId}/preco-promocional`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ precoPromocional: precoPayload }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Preço promocional atualizado!');
|
||||
setPrecosOriginais(prev => ({
|
||||
...prev,
|
||||
[produtoId]: precoFormatado
|
||||
}));
|
||||
setPrecosPromocionais(prev => ({
|
||||
...prev,
|
||||
[produtoId]: precoFormatado
|
||||
}));
|
||||
setPrecosAlterados(prev => ({
|
||||
...prev,
|
||||
[produtoId]: false
|
||||
}));
|
||||
setProdutos(prev =>
|
||||
prev.map(produto =>
|
||||
produto.id === produtoId
|
||||
? { ...produto, preco_promocional: precoPayload }
|
||||
: produto
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.message || 'Erro ao atualizar preço');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar preço:', error);
|
||||
toast.error('Erro ao atualizar preço');
|
||||
} finally {
|
||||
setPrecosSalvando(prev => ({
|
||||
...prev,
|
||||
[produtoId]: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const salvarConfiguracoes = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/configuracoes/catalogo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(catalogoConfig),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Configurações salvas com sucesso!');
|
||||
} else {
|
||||
throw new Error('Erro ao salvar configurações');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar configurações:', error);
|
||||
toast.error('Erro ao salvar configurações');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProdutoVisivel = async (produtoId, visivelAtual) => {
|
||||
try {
|
||||
const response = await fetch(`/api/produtos/${produtoId}/visibilidade`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ visivelCatalogo: !visivelAtual }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Visibilidade atualizada!');
|
||||
carregarProdutos();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao atualizar visibilidade:', error);
|
||||
toast.error('Erro ao atualizar visibilidade');
|
||||
}
|
||||
};
|
||||
|
||||
const abrirGerenciarFotos = async (produto) => {
|
||||
setProdutoSelecionado(produto);
|
||||
setModalFotosOpen(true);
|
||||
await carregarFotosAdicionais(produto.id);
|
||||
};
|
||||
|
||||
const carregarFotosAdicionais = async (produtoId) => {
|
||||
try {
|
||||
const response = await fetch(`/api/produtos/${produtoId}/fotos-catalogo`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setFotosAdicionais(data.fotos || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar fotos adicionais:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadFoto = async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file || !produtoSelecionado) return;
|
||||
|
||||
// Validar tipo de arquivo
|
||||
const tiposPermitidos = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
||||
if (!tiposPermitidos.includes(file.type)) {
|
||||
toast.error('Tipo de arquivo não permitido. Use JPEG, PNG, WebP ou GIF');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tamanho (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('Arquivo muito grande. Máximo 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('foto', file);
|
||||
|
||||
try {
|
||||
setUploadingPhoto(true);
|
||||
const response = await fetch(`/api/produtos/${produtoSelecionado.id}/fotos-catalogo`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Foto adicionada com sucesso!');
|
||||
await carregarFotosAdicionais(produtoSelecionado.id);
|
||||
await carregarProdutos();
|
||||
// Limpar input
|
||||
event.target.value = '';
|
||||
} else {
|
||||
const errorMsg = data.error || 'Erro ao fazer upload';
|
||||
console.error('Erro do servidor:', data);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao fazer upload:', error);
|
||||
toast.error('Erro ao adicionar foto: ' + error.message);
|
||||
} finally {
|
||||
setUploadingPhoto(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deletarFoto = async (fileName) => {
|
||||
if (!produtoSelecionado) return;
|
||||
|
||||
if (!window.confirm('Tem certeza que deseja remover esta foto?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/produtos/${produtoSelecionado.id}/fotos-catalogo/${fileName}`,
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Foto removida!');
|
||||
await carregarFotosAdicionais(produtoSelecionado.id);
|
||||
await carregarProdutos();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao deletar foto:', error);
|
||||
toast.error('Erro ao remover foto');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && produtos.length === 0) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<div>Carregando catálogo...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const produtosVisiveis = produtos.filter(p => p.visivel_catalogo);
|
||||
const produtosPromocao = produtos.filter(p => p.em_promocao && p.visivel_catalogo);
|
||||
const produtosNovidade = produtos.filter(p => p.novidade && p.visivel_catalogo);
|
||||
|
||||
return (
|
||||
<div className="site-catalogo fade-in">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1>
|
||||
<FiGlobe style={{ marginRight: '10px' }} />
|
||||
Site / Catálogo
|
||||
</h1>
|
||||
<p>Gerencie os produtos visíveis no catálogo online</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={carregarProdutos}
|
||||
>
|
||||
<FiRefreshCw />
|
||||
Atualizar
|
||||
</button>
|
||||
<a
|
||||
href="/catalogo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-primary"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<FiGlobe />
|
||||
Ver Catálogo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configurações do Catálogo */}
|
||||
<div className="config-section">
|
||||
<div className="config-header">
|
||||
<div className="config-title">
|
||||
<FiSettings className="config-icon" />
|
||||
<div>
|
||||
<h2>Configurações do Catálogo</h2>
|
||||
<p>Configure as opções de exibição do catálogo online</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-form">
|
||||
<p className="config-info-text">
|
||||
As opções de exibição do catálogo são gerenciadas automaticamente. Utilize o botão abaixo caso precise sincronizar as configurações.
|
||||
</p>
|
||||
|
||||
<div className="config-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-primary ${loading ? 'loading' : ''}`}
|
||||
onClick={salvarConfiguracoes}
|
||||
disabled={loading}
|
||||
>
|
||||
<FiSettings />
|
||||
{loading ? 'Salvando...' : 'Salvar Configurações'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Estatísticas */}
|
||||
<div className="catalogo-stats">
|
||||
<div className="stat-card">
|
||||
<FiPackage className="stat-icon" />
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{produtos.length}</span>
|
||||
<span className="stat-label">Total de Produtos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card success">
|
||||
<FiEye className="stat-icon" />
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{produtosVisiveis.length}</span>
|
||||
<span className="stat-label">Visíveis</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card warning">
|
||||
<FiEyeOff className="stat-icon" />
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{produtos.length - produtosVisiveis.length}</span>
|
||||
<span className="stat-label">Ocultos</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card" style={{ background: 'linear-gradient(135deg, #ffd89b 0%, #f59e0b 100%)', color: 'white' }}>
|
||||
<span className="stat-icon" style={{ fontSize: '2rem' }}>🏷️</span>
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{produtosPromocao.length}</span>
|
||||
<span className="stat-label">Em Promoção</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card" style={{ background: 'linear-gradient(135deg, #a8edea 0%, #3b82f6 100%)', color: 'white' }}>
|
||||
<span className="stat-icon" style={{ fontSize: '2rem' }}>✨</span>
|
||||
<div className="stat-info">
|
||||
<span className="stat-value">{produtosNovidade.length}</span>
|
||||
<span className="stat-label">Novidades</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista de Produtos */}
|
||||
<div className="products-section">
|
||||
<h2>
|
||||
<FiPackage style={{ marginRight: '10px' }} />
|
||||
Produtos do Catálogo
|
||||
</h2>
|
||||
|
||||
{produtos.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<FiPackage size={48} />
|
||||
<p>Nenhum produto cadastrado</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="products-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Foto</th>
|
||||
<th>Produto</th>
|
||||
<th>Preço Normal</th>
|
||||
<th>Preço Promocional</th>
|
||||
<th>Estoque</th>
|
||||
<th>Status</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{produtos.map((produto) => (
|
||||
<tr
|
||||
key={produto.id}
|
||||
className={!produto.visivel_catalogo ? 'row-hidden' : ''}
|
||||
>
|
||||
<td>
|
||||
<div className="table-product-image">
|
||||
{produto.foto_principal_url || produto.imagem ? (
|
||||
<img src={produto.foto_principal_url || produto.imagem} alt={produto.nome} />
|
||||
) : (
|
||||
<div className="no-image">
|
||||
<FiImage size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="table-product-info">
|
||||
<strong>{produto.nome}</strong>
|
||||
<small>{produto.marca || 'Sem marca'}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="price-normal">
|
||||
R$ {parseFloat(produto.valor_revenda || 0).toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="promo-input-wrapper">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
className={`input-preco-promo ${precosAlterados[produto.id] ? 'changed' : ''}`}
|
||||
placeholder="0.00"
|
||||
value={precosPromocionais[produto.id] ?? ''}
|
||||
onChange={(e) => handlePrecoPromocionalChange(produto.id, e.target.value)}
|
||||
onBlur={() => handlePrecoPromocionalBlur(produto.id)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn-icon promo-save-btn ${precosAlterados[produto.id] ? 'active' : ''}`}
|
||||
title={precosAlterados[produto.id] ? 'Salvar preço promocional' : 'Nenhuma alteração pendente'}
|
||||
onClick={() => salvarPrecoPromocional(produto.id)}
|
||||
disabled={!precosAlterados[produto.id] || precosSalvando[produto.id]}
|
||||
>
|
||||
{precosSalvando[produto.id] ? (
|
||||
<FiRefreshCw className="icon-spin" />
|
||||
) : (
|
||||
<FiSave />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`stock-badge ${produto.estoque_total > 0 ? 'in-stock' : 'out-stock'}`}>
|
||||
{produto.estoque_total || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="status-badges">
|
||||
<span
|
||||
className={`badge ${produto.visivel_catalogo ? 'badge-success' : 'badge-secondary'}`}
|
||||
title={produto.visivel_catalogo ? 'Visível' : 'Oculto'}
|
||||
>
|
||||
{produto.visivel_catalogo ? <FiEye /> : <FiEyeOff />}
|
||||
</span>
|
||||
<span
|
||||
className={`badge ${produto.novidade ? 'badge-info' : 'badge-light'}`}
|
||||
title="Novidade"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => toggleProdutoNovidade(produto.id, produto.novidade)}
|
||||
>
|
||||
✨ {produto.novidade ? 'NOVO' : ''}
|
||||
</span>
|
||||
<span
|
||||
className={`badge ${produto.em_promocao ? 'badge-warning' : 'badge-light'}`}
|
||||
title="Promoção"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => toggleProdutoPromocao(produto.id, produto.em_promocao)}
|
||||
>
|
||||
🏷️ {produto.em_promocao ? 'PROMO' : ''}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="table-actions">
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => toggleProdutoVisivel(produto.id, produto.visivel_catalogo)}
|
||||
title={produto.visivel_catalogo ? 'Ocultar' : 'Mostrar'}
|
||||
>
|
||||
{produto.visivel_catalogo ? <FiEyeOff /> : <FiEye />}
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => abrirGerenciarFotos(produto)}
|
||||
title="Gerenciar fotos"
|
||||
>
|
||||
<FiImage />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal de Gerenciamento de Fotos */}
|
||||
{modalFotosOpen && produtoSelecionado && (
|
||||
<div className="modal-overlay" onClick={() => setModalFotosOpen(false)}>
|
||||
<div className="modal-content-fotos" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>
|
||||
<FiImage style={{ marginRight: '10px' }} />
|
||||
Gerenciar Fotos - {produtoSelecionado.nome}
|
||||
</h2>
|
||||
<button className="modal-close" onClick={() => setModalFotosOpen(false)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="upload-section">
|
||||
<label className="upload-button">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleUploadFoto}
|
||||
disabled={uploadingPhoto}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<FiImage />
|
||||
{uploadingPhoto ? 'Enviando...' : 'Adicionar Nova Foto'}
|
||||
</label>
|
||||
<p className="upload-help">
|
||||
Adicione fotos extras que aparecerão no catálogo online
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fotos-grid">
|
||||
{fotosAdicionais.length === 0 ? (
|
||||
<div className="empty-fotos">
|
||||
<FiImage size={48} />
|
||||
<p>Nenhuma foto adicional</p>
|
||||
<span>Adicione fotos para exibir no catálogo</span>
|
||||
</div>
|
||||
) : (
|
||||
fotosAdicionais.map((foto) => (
|
||||
<div key={foto.name} className="foto-item">
|
||||
<img src={foto.url} alt={foto.name} />
|
||||
<button
|
||||
className="btn-delete-foto"
|
||||
onClick={() => deletarFoto(foto.name)}
|
||||
title="Remover foto"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteCatalogo;
|
||||
File diff suppressed because it is too large
Load Diff
179
client/src/styles/pedidos-catalogo.css
Normal file
179
client/src/styles/pedidos-catalogo.css
Normal file
@@ -0,0 +1,179 @@
|
||||
.pedidos-catalogo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.pedidos-lista {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.pedido-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.pedido-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.pedido-id, .pedido-data {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.pedido-cliente {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pedido-cliente .cliente-nome {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.pedido-cliente .cliente-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pedido-cliente .cliente-info span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pedido-itens {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pedido-itens h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.pedido-itens-lista {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pedido-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pedido-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pedido-item-info strong {
|
||||
font-size: 15px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.pedido-item-meta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pedido-item-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pedido-item-valores {
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.pedido-item-valores strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.pedido-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pedido-total {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pedido-total span {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.pedido-total strong {
|
||||
font-size: 18px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pedidos-lista {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pedido-card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@@ -154,3 +154,40 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-whatsapp-pix {
|
||||
margin-left: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: #22c55e;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-whatsapp-pix:hover {
|
||||
background-color: #16a34a;
|
||||
}
|
||||
|
||||
.pix-code-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pix-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pix-actions .btn {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
324
client/src/styles/site-catalogo-table.css
Normal file
324
client/src/styles/site-catalogo-table.css
Normal file
@@ -0,0 +1,324 @@
|
||||
/* ====================
|
||||
TABELA DE PRODUTOS
|
||||
==================== */
|
||||
|
||||
.products-table {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.products-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.products-table thead {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.products-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.products-table tbody tr {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.products-table tbody tr:hover {
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.products-table tbody tr.row-hidden {
|
||||
opacity: 0.5;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.products-table td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Imagem do Produto na Tabela */
|
||||
.table-product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.table-product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.table-product-image .no-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f7fafc;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
/* Info do Produto */
|
||||
.table-product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.table-product-info strong {
|
||||
font-size: 1rem;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.table-product-info small {
|
||||
font-size: 0.85rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Preços */
|
||||
.price-normal {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.input-preco-promo {
|
||||
width: 120px;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #e53e3e;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-preco-promo:focus {
|
||||
outline: none;
|
||||
border-color: #f56565;
|
||||
box-shadow: 0 0 0 3px rgba(245, 101, 101, 0.1);
|
||||
}
|
||||
|
||||
.input-preco-promo::placeholder {
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
.input-preco-promo.changed {
|
||||
border-color: #f59e0b;
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.promo-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.promo-save-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #4b5563;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.promo-save-btn.active {
|
||||
background: #4f46e5;
|
||||
border-color: #4338ca;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.promo-save-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.promo-save-btn:not(:disabled):hover {
|
||||
background: #4338ca;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.icon-spin {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge de Estoque */
|
||||
.stock-badge {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-badge.in-stock {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.stock-badge.out-stock {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badges {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
border-color: #9ae6b4;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #4a5568;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: #bee3f8;
|
||||
color: #2c5282;
|
||||
border-color: #90cdf4;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #feebc8;
|
||||
color: #7c2d12;
|
||||
border-color: #fbd38d;
|
||||
}
|
||||
|
||||
.badge-light {
|
||||
background: #f7fafc;
|
||||
color: #a0aec0;
|
||||
border-color: #e2e8f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.badge-light:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e0;
|
||||
}
|
||||
|
||||
/* Ações da Tabela */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 0.6rem;
|
||||
border: none;
|
||||
background: #f7fafc;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Responsividade */
|
||||
@media (max-width: 1024px) {
|
||||
.products-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.products-table table {
|
||||
min-width: 900px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.products-table th,
|
||||
.products-table td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-product-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.input-preco-promo {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.status-badges {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animações */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.products-table tbody tr {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.products-table tbody tr:nth-child(even) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
666
client/src/styles/site-catalogo.css
Normal file
666
client/src/styles/site-catalogo.css
Normal file
@@ -0,0 +1,666 @@
|
||||
.site-catalogo {
|
||||
padding: 1rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 2rem;
|
||||
color: #2d3748;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #718096;
|
||||
margin: 0.5rem 0 0 0;
|
||||
}
|
||||
|
||||
/* Config Section */
|
||||
.config-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.config-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.config-title h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.config-title p {
|
||||
margin: 0.5rem 0 0 0;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.config-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #cbd5e0;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background-color: #667eea;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Estatísticas */
|
||||
.catalogo-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.stat-card.success {
|
||||
border-left-color: #48bb78;
|
||||
}
|
||||
|
||||
.stat-card.warning {
|
||||
border-left-color: #ed8936;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-card.success .stat-icon {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.stat-card.warning .stat-icon {
|
||||
color: #ed8936;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Products Section */
|
||||
.products-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.products-section h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
color: #2d3748;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.product-card.product-hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: #f7fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-no-image {
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.product-info h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #2d3748;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
font-size: 0.9rem;
|
||||
color: #718096;
|
||||
margin: 0 0 1rem 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.product-stock {
|
||||
font-size: 0.9rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
padding: 1rem;
|
||||
background: #f7fafc;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-visibility,
|
||||
.btn-manage-photos {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-visibility.visible {
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-visibility.visible:hover {
|
||||
background: #38a169;
|
||||
}
|
||||
|
||||
.btn-visibility.hidden {
|
||||
background: #cbd5e0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.btn-visibility.hidden:hover {
|
||||
background: #a0aec0;
|
||||
}
|
||||
|
||||
.btn-manage-photos {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-manage-photos:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: #a0aec0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #cbd5e0;
|
||||
}
|
||||
|
||||
.btn.loading {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
font-size: 1.2rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Fade In Animation */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal de Fotos */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content-fotos {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.upload-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.upload-help {
|
||||
margin-top: 0.5rem;
|
||||
color: #718096;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.fotos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.foto-item {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 2px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.foto-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.foto-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.btn-delete-foto {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(220, 53, 69, 0.9);
|
||||
color: white;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.foto-item:hover .btn-delete-foto {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-delete-foto:hover {
|
||||
background: #dc3545;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.empty-fotos {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: #a0aec0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-fotos p {
|
||||
margin: 1rem 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-fotos span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.catalogo-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.fotos-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.modal-content-fotos {
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,163 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Parcelas individuais na visualização */
|
||||
.parcelas-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.parcela-card {
|
||||
background: white;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.parcela-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.parcela-card.pago {
|
||||
border-color: #28a745;
|
||||
background: #f0f9f4;
|
||||
}
|
||||
|
||||
.parcela-card.vencida {
|
||||
border-color: #dc3545;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.parcela-card.pendente {
|
||||
border-color: #ffc107;
|
||||
background: #fffdf0;
|
||||
}
|
||||
|
||||
.parcela-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.parcela-numero {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.parcela-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.parcela-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.parcela-info > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.parcela-info strong {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.parcela-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-pix {
|
||||
background: linear-gradient(135deg, #00c4cc 0%, #00a8b5 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-pix:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #00a8b5 0%, #008c99 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 196, 204, 0.3);
|
||||
}
|
||||
|
||||
.btn-pix:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.parcelas-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.parcela-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.parcela-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.parcela-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Estilos para linhas de parcelas na tabela */
|
||||
.parcela-row {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 3px solid #ffc107;
|
||||
}
|
||||
|
||||
.parcela-row:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
background-color: #e7f3ff;
|
||||
font-weight: bold;
|
||||
border-top: 2px solid #007bff;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.total-row td {
|
||||
padding: 12px 8px !important;
|
||||
}
|
||||
|
||||
.parcela-row td[rowspan] {
|
||||
vertical-align: top;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Estilos para coluna de produtos na tabela de vendas */
|
||||
.sale-products {
|
||||
max-width: 250px;
|
||||
|
||||
Reference in New Issue
Block a user