diff --git a/README.md b/README.md index 4179618d..079b9fe6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,19 @@ Principais endpoints: - `POST orders` – cria pedido `{ name, phone, address, table, type, items, total, status?, payment? }`. - `PATCH orders/:id/status` – atualiza status. +### Resetar o banco com os dados padrão + +- As credenciais de admin usam os mesmos valores de `PGUSER`/`PGPASSWORD` (ou `ADMIN_USER`/`ADMIN_PASSWORD`). Ajuste as variáveis de ambiente se o erro de conexão indicar senha incorreta. +- Para restaurar as tabelas e dados de exemplo, execute: + +```bash +cd server +npm install +npm run db:reset +``` + +- Também é possível disparar o reset via API autenticada: `POST /admin/reset-database` com `{ "username": "", "password": "" }`. + ### pgAdmin (local) Instale pgAdmin localmente ou utilize a imagem oficial. Para conectar ao banco local, crie um novo servidor no pgAdmin com: diff --git a/server/databaseReset.js b/server/databaseReset.js new file mode 100644 index 00000000..d708ccdb --- /dev/null +++ b/server/databaseReset.js @@ -0,0 +1,25 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { pool } from './db.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export async function resetDatabase() { + const schemaPath = path.join(__dirname, 'schema.sql'); + const sql = fs.readFileSync(schemaPath, 'utf8'); + await pool.query(sql); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + resetDatabase() + .then(() => { + console.log('Banco resetado e populado com dados iniciais.'); + }) + .catch((err) => { + console.error('Falha ao resetar banco:', err.message); + process.exitCode = 1; + }) + .finally(() => pool.end()); +} diff --git a/server/index.js b/server/index.js index 1d50694b..a4911b03 100644 --- a/server/index.js +++ b/server/index.js @@ -1,6 +1,7 @@ import express from "express"; import cors from "cors"; import { pool } from "./db.js"; +import { resetDatabase } from "./databaseReset.js"; const app = express(); const port = process.env.PORT || 4000; @@ -8,6 +9,13 @@ const port = process.env.PORT || 4000; app.use(cors()); app.use(express.json()); +const validUser = process.env.ADMIN_USER || process.env.PGUSER || "postgres"; +const validPassword = + process.env.ADMIN_PASSWORD || process.env.PGPASSWORD || "postgres"; + +const isValidAdmin = (username, password) => + username === validUser && password === validPassword; + const mapOrderRow = (row) => ({ id: row.id, name: row.customer_name, @@ -30,36 +38,86 @@ const mapCustomerRow = (row) => ({ phone: row.phone, }); -app.get("/products", async (_, res) => { +const defaultOwnerId = process.env.DEFAULT_OWNER_ID || "espetinhodatony"; + +const getOwnerId = (req) => + (req.headers["x-owner-id"] || req.query.ownerId || req.body?.ownerId || "") + .toString() + .trim(); + +const requireOwner = (req, res) => { + const ownerId = getOwnerId(req); + if (!ownerId) { + res.status(400).json({ error: "ownerId é obrigatório" }); + return null; + } + return ownerId; +}; + +app.get("/products", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const result = await pool.query( - "SELECT * FROM products ORDER BY category, name" + "SELECT * FROM products WHERE owner_id = $1 ORDER BY category, name", + [ownerId] ); res.json(result.rows); }); app.post("/login", (req, res) => { - const { username, password } = req.body || {}; + const { username, password, espetoId } = req.body || {}; - const validUser = - process.env.ADMIN_USER || process.env.PGUSER || "postgres"; - const validPassword = - process.env.ADMIN_PASSWORD || process.env.PGPASSWORD || "postgres"; + if (isValidAdmin(username, password)) { + const ownerId = (espetoId || defaultOwnerId).trim(); + if (!ownerId) { + res.status(400).json({ message: "Informe o ID do espeto" }); + return; + } - if (username === validUser && password === validPassword) { - res.json({ token: "ok", name: "Administrador" }); + res.json({ + token: "ok", + name: process.env.ADMIN_NAME || "Administrador", + ownerId, + }); return; } res.status(401).json({ message: "Credenciais inválidas" }); }); +app.post("/admin/reset-database", async (req, res) => { + const { username, password } = req.body || {}; + + if (!isValidAdmin(username, password)) { + res.status(401).json({ message: "Credenciais inválidas" }); + return; + } + + try { + await resetDatabase(); + res.json({ + message: "Banco resetado e populado com dados iniciais.", + }); + } catch (error) { + console.error("Erro ao resetar banco", error); + res.status(500).json({ + error: "Falha ao resetar banco", + detail: error.message, + }); + } +}); + app.get(["/customers", "/api/customers"], async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const search = (req.query.search || "").toLowerCase(); - let query = "SELECT * FROM customers"; - const params = []; + let query = "SELECT * FROM customers WHERE owner_id = $1"; + const params = [ownerId]; if (search) { - query += " WHERE LOWER(name) LIKE $1"; + query += " AND LOWER(name) LIKE $2"; params.push(`%${search}%`); } @@ -70,6 +128,9 @@ app.get(["/customers", "/api/customers"], async (req, res) => { }); app.post("/products", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const { name, price, @@ -80,14 +141,25 @@ app.post("/products", async (req, res) => { } = req.body; const query = - "INSERT INTO products (name, price, category, description, active, image_url) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *"; + "INSERT INTO products (owner_id, name, price, category, description, active, image_url) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *"; - const values = [name, price, category, description, active, imageUrl]; + const values = [ + ownerId, + name, + price, + category, + description, + active, + imageUrl, + ]; const result = await pool.query(query, values); res.status(201).json(result.rows[0]); }); app.put("/products/:id", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const { id } = req.params; const { name, @@ -99,34 +171,70 @@ app.put("/products/:id", async (req, res) => { } = req.body; const query = - "UPDATE products SET name = $1, price = $2, category = $3, description = $4, active = $5, image_url = $6 WHERE id = $7 RETURNING *"; + "UPDATE products SET name = $1, price = $2, category = $3, description = $4, active = $5, image_url = $6 WHERE id = $7 AND owner_id = $8 RETURNING *"; - const values = [name, price, category, description, active, imageUrl, id]; + const values = [ + name, + price, + category, + description, + active, + imageUrl, + id, + ownerId, + ]; const result = await pool.query(query, values); + if (!result.rowCount) { + res.status(404).json({ error: "Produto não encontrado para este espeto" }); + return; + } res.json(result.rows[0]); }); app.delete("/products/:id", async (req, res) => { - await pool.query("DELETE FROM products WHERE id = $1", [req.params.id]); + const ownerId = requireOwner(req, res); + if (!ownerId) return; + + const result = await pool.query( + "DELETE FROM products WHERE id = $1 AND owner_id = $2", + [req.params.id, ownerId] + ); + + if (!result.rowCount) { + res.status(404).json({ error: "Produto não encontrado para este espeto" }); + return; + } + res.status(204).send(); }); -app.get("/orders", async (_, res) => { +app.get("/orders", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const result = await pool.query( - "SELECT * FROM orders ORDER BY created_at DESC" + "SELECT * FROM orders WHERE owner_id = $1 ORDER BY created_at DESC", + [ownerId] ); res.json(result.rows.map(mapOrderRow)); }); -app.get("/orders/queue", async (_, res) => { +app.get("/orders/queue", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const result = await pool.query( - "SELECT * FROM orders WHERE status IN ('pending', 'preparing') ORDER BY created_at ASC" + "SELECT * FROM orders WHERE owner_id = $1 AND status IN ('pending', 'preparing') ORDER BY created_at ASC", + [ownerId] ); res.json(result.rows.map(mapOrderRow)); }); app.post("/orders", async (req, res) => { console.log("DEBUG BODY:", req.body); + const ownerId = requireOwner(req, res); + if (!ownerId) return; + try { const { name, @@ -144,6 +252,7 @@ app.post("/orders", async (req, res) => { const query = ` INSERT INTO orders ( + owner_id, customer_name, phone, address, @@ -156,11 +265,12 @@ app.post("/orders", async (req, res) => { created_at, date_string ) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING *; `; const values = [ + ownerId, name, phone, address ?? null, @@ -178,11 +288,11 @@ app.post("/orders", async (req, res) => { if (name) { await pool.query( - `INSERT INTO customers (name, phone, updated_at) - VALUES ($1, $2, NOW()) - ON CONFLICT (name) + `INSERT INTO customers (owner_id, name, phone, updated_at) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (owner_id, name) DO UPDATE SET phone = COALESCE(EXCLUDED.phone, customers.phone), updated_at = NOW();`, - [name, phone ?? null] + [ownerId, name, phone ?? null] ); } @@ -194,16 +304,28 @@ app.post("/orders", async (req, res) => { }); app.patch("/orders/:id/status", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const { id } = req.params; const { status } = req.body; const result = await pool.query( - "UPDATE orders SET status = $1 WHERE id = $2 RETURNING *", - [status, id] + "UPDATE orders SET status = $1 WHERE id = $2 AND owner_id = $3 RETURNING *", + [status, id, ownerId] ); + + if (!result.rowCount) { + res.status(404).json({ error: "Pedido não encontrado para este espeto" }); + return; + } + res.json(mapOrderRow(result.rows[0])); }); app.patch("/orders/:id", async (req, res) => { + const ownerId = requireOwner(req, res); + if (!ownerId) return; + const { id } = req.params; const { items, total } = req.body; @@ -219,11 +341,30 @@ app.patch("/orders/:id", async (req, res) => { : sanitizedItems.reduce((sum, item) => sum + (item.price || 0) * (item.qty || 0), 0); const result = await pool.query( - "UPDATE orders SET items = $1, total = $2 WHERE id = $3 RETURNING *", - [JSON.stringify(sanitizedItems), computedTotal, id] + "UPDATE orders SET items = $1, total = $2 WHERE id = $3 AND owner_id = $4 RETURNING *", + [JSON.stringify(sanitizedItems), computedTotal, id, ownerId] ); + if (!result.rowCount) { + res.status(404).json({ error: "Pedido não encontrado para este espeto" }); + return; + } + res.json(mapOrderRow(result.rows[0])); }); -app.listen(port, () => console.log(`API escutando na porta ${port}`)); +const startServer = async () => { + try { + await pool.query("SELECT 1"); + } catch (error) { + console.error( + "Falha ao conectar no banco. Verifique PGUSER/PGPASSWORD/PGDATABASE.", + error.message + ); + process.exit(1); + } + + app.listen(port, () => console.log(`API escutando na porta ${port}`)); +}; + +startServer(); diff --git a/server/package.json b/server/package.json index 78df3cb4..b48726c7 100644 --- a/server/package.json +++ b/server/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "scripts": { - "start": "node index.js" + "start": "node index.js", + "db:reset": "node databaseReset.js" }, "dependencies": { "cors": "^2.8.5", diff --git a/server/schema.sql b/server/schema.sql index c4e8c12e..1bfe870f 100644 --- a/server/schema.sql +++ b/server/schema.sql @@ -1,8 +1,14 @@ +-- Reinicia o banco para os dados de exemplo +DROP TABLE IF EXISTS orders; +DROP TABLE IF EXISTS customers; +DROP TABLE IF EXISTS products; + -- =============================== -- TABELA DE PRODUTOS -- =============================== CREATE TABLE IF NOT EXISTS products ( id SERIAL PRIMARY KEY, + owner_id TEXT NOT NULL DEFAULT 'default_owner', name TEXT NOT NULL, price NUMERIC(10,2) NOT NULL, category TEXT NOT NULL, @@ -17,6 +23,7 @@ CREATE TABLE IF NOT EXISTS products ( -- =============================== CREATE TABLE IF NOT EXISTS orders ( id SERIAL PRIMARY KEY, + owner_id TEXT NOT NULL DEFAULT 'default_owner', customer_name TEXT, phone TEXT, address TEXT, @@ -35,73 +42,75 @@ CREATE TABLE IF NOT EXISTS orders ( -- =============================== CREATE TABLE IF NOT EXISTS customers ( id SERIAL PRIMARY KEY, - name TEXT UNIQUE, + owner_id TEXT NOT NULL DEFAULT 'default_owner', + name TEXT NOT NULL, phone TEXT, created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() + updated_at TIMESTAMP DEFAULT NOW(), + CONSTRAINT customers_owner_name_unique UNIQUE(owner_id, name) ); -- =============================== -- POPULAR PRODUTOS (ESPETOS) -- =============================== -INSERT INTO products (name, price, category, description, active, image_url) VALUES -('Alcatra c/ Bacon', 10.50, 'espetos', NULL, true, +INSERT INTO products (owner_id, name, price, category, description, active, image_url) VALUES +('espetinhodatony', 'Alcatra c/ Bacon', 10.50, 'espetos', NULL, true, 'https://i0.wp.com/espetinhodesucesso.com/wp-content/uploads/2018/03/espetinho-de-carne-com-bacon.jpg?w=750&ssl=1'), -('Frango c/ Bacon', 10.50, 'espetos', NULL, true, +('espetinhodatony', 'Frango c/ Bacon', 10.50, 'espetos', NULL, true, 'https://www.vivaochurrasco.com.br/wp-content/uploads/2019/05/frangocombacon.jpg'), -('Carne Bovina', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Carne Bovina', 8.50, 'espetos', NULL, true, 'https://eliteprimebeef.com.br/wp-content/uploads/2016/04/espetinho-de-carne-600x400.jpg'), -('Frango', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Frango', 8.50, 'espetos', NULL, true, 'https://www.vivaespetos.com.br/wp-content/uploads/2019/05/frango.jpg'), -('Coração', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Coração', 8.50, 'espetos', NULL, true, 'https://d1muf25xaso8hp.cloudfront.net/https://img.criativodahora.com.br/homologacao/thumbs/2025/07/25/01983fc9-c59b-73dd-93d5-ec62564dd71c.jpg?w=1000&h=&auto=compress&dpr=1&fit=max'), -('Linguiça', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Linguiça', 8.50, 'espetos', NULL, true, 'https://as1.ftcdn.net/jpg/03/51/38/68/1000_F_351386865_CPy3Ir6Go8waqfiNdUWp0aK1YaGU9FdB.jpg'), -('Kafta Bovina', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Kafta Bovina', 8.50, 'espetos', NULL, true, 'https://cozinhasimples.com.br/wp-content/uploads/kafta-no-palito-cozinha-simples.jpg'), -('Kafta de Frango c/ Queijo', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Kafta de Frango c/ Queijo', 8.50, 'espetos', NULL, true, 'https://s3-sa-east-1.amazonaws.com/loja2/191f40e696fa55001362045933e28607.jpg'), -('Torresmo', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Torresmo', 8.50, 'espetos', NULL, true, 'https://www.minhasreceitas.blog.br/wp-content/webp-express/webp-images/uploads/2021/07/pork-skewer-over-fire-flames-panceta.jpg.webp'), -('Pão de Alho', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Pão de Alho', 8.50, 'espetos', NULL, true, 'https://i.pinimg.com/736x/73/9b/c6/739bc692563f9cfa5ab3b2ce91c25e11.jpg'), -('Queijo Coalho', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Queijo Coalho', 8.50, 'espetos', NULL, true, 'https://content.paodeacucar.com/wp-content/uploads/2019/11/queijo-coalho-churrasco.jpg'), -('Costela Bovina', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Costela Bovina', 8.50, 'espetos', NULL, true, 'http://bertioga.tudoem.com.br/assets/img/anuncio/espeto_de_costela_bovina_4.webp'), -('Tulipa de Frango', 8.50, 'espetos', NULL, true, +('espetinhodatony', 'Tulipa de Frango', 8.50, 'espetos', NULL, true, 'https://andinacocacola.vtexassets.com/arquivos/ids/157883-1200-auto?v=638412015595530000&width=1200&height=auto&aspect=true'); -- =============================== -- POPULAR PRODUTOS (BEBIDAS) -- =============================== -INSERT INTO products (name, price, category, description, active, image_url) VALUES -('Refrigerante Lata', 7.50, 'bebidas', NULL, true, +INSERT INTO products (owner_id, name, price, category, description, active, image_url) VALUES +('espetinhodatony', 'Refrigerante Lata', 7.50, 'bebidas', NULL, true, 'https://alloydeliveryimages.s3.sa-east-1.amazonaws.com/item_images/11542/669add5769e6e2x9g4.webp'), -('Refrigerante Mantiqueira', 8.50, 'bebidas', NULL, true, +('espetinhodatony', 'Refrigerante Mantiqueira', 8.50, 'bebidas', NULL, true, 'https://refrigerantesmantiqueira.com.br/wp-content/uploads/2023/11/guarana-2-litros.webp'), -('Suco', 7.50, 'bebidas', NULL, true, +('espetinhodatony', 'Suco', 7.50, 'bebidas', NULL, true, 'https://boomi.b-cdn.net/wp-content/uploads/2024/11/Sucos-de-frutas-sao-ou-nao-sao-saudaveis.png.webp'), -('Água', 3.00, 'bebidas', NULL, true, +('espetinhodatony', 'Água', 3.00, 'bebidas', NULL, true, 'https://andinacocacola.vtexassets.com/arquivos/ids/157883-1200-auto?v=638412015595530000&width=1200&height=auto&aspect=true'), -('Cerveja Heineken', 7.00, 'bebidas', NULL, true, +('espetinhodatony', 'Cerveja Heineken', 7.00, 'bebidas', NULL, true, 'https://res.cloudinary.com/piramides/image/upload/c_fill,h_564,w_395/v1/products/16195-heineken-lata-269ml-normal-8un.20251024104230.png?_a=BAAAV6GX'), -('Cerveja Skol', 6.00, 'bebidas', NULL, true, +('espetinhodatony', 'Cerveja Skol', 6.00, 'bebidas', NULL, true, 'https://savegnagoio.vtexassets.com/arquivos/ids/451158-1200-auto?v=638610711235400000&width=1200&height=auto&aspect=true'); diff --git a/src/App.js b/src/App.js index eb06adb0..7a7891a0 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ShoppingCart, Send, @@ -20,6 +20,8 @@ import { SuccessView } from './components/Client/SuccessView'; import { DashboardView } from './components/Admin/DashboardView'; import { ProductManager } from './components/Admin/ProductManager'; import { GrillQueue } from './components/Admin/GrillQueue'; +import { BrandingSettings } from './components/Admin/BrandingSettings'; +import { apiClient } from './config/apiClient'; import { formatCurrency, formatOrderStatus, @@ -35,6 +37,29 @@ const initialCustomer = { name: '', phone: formatPhoneInput('', DEFAULT_AREA_COD const defaultPaymentMethod = 'debito'; const WHATSAPP_NUMBER = process.env.REACT_APP_WHATSAPP_NUMBER || '5512996797210'; const PIX_KEY = process.env.REACT_APP_PIX_KEY || ''; +const defaultBranding = { + brandName: 'Datony', + espetoId: 'espetinhodatony', + logoUrl: '/logo-datony.svg', + primaryColor: '#b91c1c', + accentColor: '#111827', + tagline: 'O melhor churrasco da região • Peça agora', + instagram: 'espetinhodatony', +}; + +const brandingStorageKey = (ownerId) => `brandingSettings:${ownerId || defaultBranding.espetoId}`; + +const getPersistedBranding = (ownerId = defaultBranding.espetoId) => { + const saved = localStorage.getItem(brandingStorageKey(ownerId)); + if (!saved) return { ...defaultBranding, espetoId: ownerId }; + try { + const parsed = JSON.parse(saved); + return { ...defaultBranding, espetoId: ownerId, ...parsed }; + } catch (error) { + console.error('Erro ao carregar branding salvo', error); + return { ...defaultBranding, espetoId: ownerId }; + } +}; function App() { const [user, setUser] = useState(null); @@ -47,8 +72,29 @@ function App() { const [customer, setCustomer] = useState(initialCustomer); const [paymentMethod, setPaymentMethod] = useState(defaultPaymentMethod); const [lastOrder, setLastOrder] = useState(null); - const [loginForm, setLoginForm] = useState({ username: '', password: '' }); + const [loginForm, setLoginForm] = useState({ username: '', password: '', espetoId: defaultBranding.espetoId }); const [loginError, setLoginError] = useState(''); + const [branding, setBranding] = useState(() => getPersistedBranding(defaultBranding.espetoId)); + const [reportFilter, setReportFilter] = useState({ + mode: 'all', + month: '', + year: new Date().getFullYear().toString(), + start: '', + end: '', + }); + + const requireAuth = useCallback( + (nextView) => { + if (user) { + setView(nextView); + return; + } + + setLoginError('Faça login para acessar essa área protegida.'); + setView('login'); + }, + [user] + ); const formatOrderDate = (order) => { if (order.createdAt?.seconds) return new Date(order.createdAt.seconds * 1000).toLocaleString('pt-BR'); @@ -57,24 +103,122 @@ function App() { return order.dateString || ''; }; + const getOrderDateValue = useCallback((order) => { + if (order.createdAt?.seconds) return new Date(order.createdAt.seconds * 1000); + if (order.createdAt) return new Date(order.createdAt); + if (order.timestamp) return new Date(order.timestamp); + if (order.dateString) return new Date(order.dateString); + return null; + }, []); + useEffect(() => { const savedSession = localStorage.getItem('adminSession'); + let initialOwnerId = defaultBranding.espetoId; + if (savedSession) { const parsedSession = JSON.parse(savedSession); + initialOwnerId = parsedSession.ownerId || initialOwnerId; setUser(parsedSession); setView('admin'); + setLoginForm((prev) => ({ ...prev, espetoId: parsedSession.ownerId || prev.espetoId })); + setBranding(getPersistedBranding(initialOwnerId)); } + apiClient.setOwnerId(initialOwnerId); + const unsubProd = productService.subscribe(setProducts); - const unsubOrders = orderService.subscribeAll(setOrders); - customerService.fetchAll().then(setCustomers).catch(() => setCustomers([])); return () => { unsubProd(); - unsubOrders(); }; }, []); + useEffect(() => { + if (!user) { + setOrders([]); + return undefined; + } + + const unsubOrders = orderService.subscribeAll(setOrders); + return () => { + unsubOrders(); + }; + }, [user?.ownerId]); + + useEffect(() => { + if (!user) { + setCustomers([]); + return; + } + + customerService.fetchAll().then(setCustomers).catch(() => setCustomers([])); + }, [user?.ownerId]); + + const resolvedOwnerId = useMemo( + () => user?.ownerId || branding?.espetoId || defaultBranding.espetoId, + [user?.ownerId, branding?.espetoId] + ); + + useEffect(() => { + if (!resolvedOwnerId) return; + apiClient.setOwnerId(resolvedOwnerId); + setBranding((prev) => { + if (prev.espetoId === resolvedOwnerId) return prev; + return getPersistedBranding(resolvedOwnerId); + }); + }, [resolvedOwnerId]); + + useEffect(() => { + if (!user && (view === 'admin' || view === 'grill')) { + setLoginError('Faça login para acessar essa área protegida.'); + setView('login'); + } + }, [user, view]); + + useEffect(() => { + const storageKey = brandingStorageKey(branding.espetoId || resolvedOwnerId); + localStorage.setItem(storageKey, JSON.stringify(branding)); + document.documentElement.style.setProperty('--primary-color', branding.primaryColor || defaultBranding.primaryColor); + document.documentElement.style.setProperty('--accent-color', branding.accentColor || branding.primaryColor || defaultBranding.accentColor); + }, [branding, resolvedOwnerId]); + const cartTotal = useMemo(() => Object.values(cart).reduce((acc, item) => acc + item.price * item.qty, 0), [cart]); + const brandInitials = useMemo( + () => branding.brandName?.split(' ').map((part) => part?.[0]).join('').slice(0, 2).toUpperCase() || 'ED', + [branding.brandName] + ); + const instagramHandle = useMemo(() => (branding.instagram ? `@${branding.instagram.replace('@', '')}` : ''), [branding.instagram]); + + const filteredOrders = useMemo(() => { + if (!reportFilter.mode || reportFilter.mode === 'all') return orders; + + return orders.filter((order) => { + const date = getOrderDateValue(order); + if (!date) return false; + + if (reportFilter.mode === 'month' && reportFilter.month) { + const monthString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + return monthString === reportFilter.month; + } + + if (reportFilter.mode === 'year' && reportFilter.year) { + return String(date.getFullYear()) === reportFilter.year; + } + + if (reportFilter.mode === 'range' && reportFilter.start && reportFilter.end) { + const startDate = new Date(reportFilter.start); + const endDate = new Date(reportFilter.end); + endDate.setHours(23, 59, 59, 999); + return date >= startDate && date <= endDate; + } + + return true; + }); + }, [orders, reportFilter, getOrderDateValue]); + + const filteredTotal = useMemo( + () => filteredOrders.reduce((acc, order) => acc + (order.total || 0), 0), + [filteredOrders] + ); const updateCart = (item, qty) => { setCart((previous) => { @@ -183,9 +327,10 @@ function App() { setLoginError(''); try { - const session = await authService.login(loginForm.username, loginForm.password); - const sessionData = { ...session, username: loginForm.username }; + const session = await authService.login(loginForm.username, loginForm.password, loginForm.espetoId); + const sessionData = { ...session, username: loginForm.username, espetoId: loginForm.espetoId }; localStorage.setItem('adminSession', JSON.stringify(sessionData)); + setBranding(getPersistedBranding(sessionData.ownerId || loginForm.espetoId)); setUser(sessionData); setView('admin'); } catch (error) { @@ -193,14 +338,24 @@ function App() { } }; + const updateBranding = (updater) => { + setBranding((prev) => { + const nextState = typeof updater === 'function' ? updater(prev) : { ...prev, ...updater }; + return nextState; + }); + }; + const logout = () => { localStorage.removeItem('adminSession'); setUser(null); - setLoginForm({ username: '', password: '' }); + setLoginForm({ username: '', password: '', espetoId: defaultBranding.espetoId }); + const fallbackBranding = getPersistedBranding(defaultBranding.espetoId); + apiClient.setOwnerId(fallbackBranding.espetoId); + setBranding(fallbackBranding); setView('menu'); }; - const exportOrders = () => { + const exportOrders = (dataset = filteredOrders) => { const headers = [ { key: 'data', label: 'Data' }, { key: 'cliente', label: 'Cliente' }, @@ -211,7 +366,7 @@ function App() { { key: 'status', label: 'Status' }, ]; - const rows = orders.map((order) => ({ + const rows = dataset.map((order) => ({ data: formatOrderDate(order), cliente: order.name, telefone: order.phone, @@ -230,9 +385,9 @@ function App() {