'use strict' const Cotacao = require('../models/Cotacao') const Pedido = require('../models/Pedido') const Produto = require('../models/Produto') const Promotion = require('../models/Promotion') const CotacaoFlashPromo = require('../models/CotacaoFlashPromo') const Notification = require('../models/Notification') const { queryOne: dbQueryOne, query: dbQuery } = require('../database/postgres') // Notifica o WA-Inbox que um agente foi atribuído a um pedido (best-effort) async function notifyNwAssignAgent(pedidoId, accountId) { if (!accountId) return try { const cfgRows = await dbQuery(`SELECT key, value FROM plugin_configs WHERE plugin_id='newwhats'`) const cfg = {} for (const r of cfgRows) cfg[r.key] = r.value if (!cfg.newwhats_url || !cfg.integration_key) return // Busca chat_id vinculado ao pedido e dados do account const pedido = await dbQueryOne('SELECT chat_id FROM pedidos WHERE id=$1', [pedidoId]) const account = await dbQueryOne('SELECT id, name FROM accounts WHERE id=$1', [accountId]) if (!pedido?.chat_id || !account) return await fetch(`${cfg.newwhats_url}/api/ext/v1/protocol/assign-agent`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'x-nw-key': cfg.integration_key }, body: JSON.stringify({ chatId: pedido.chat_id, agentId: String(account.id), agentName: account.name }), }) } catch { /* best-effort: nunca bloqueia o fluxo principal */ } } // Aplica preço promocional se existir (PROMO-03) async function resolverPreco(produtoId, tenantId) { const prod = await Produto.findById(produtoId) if (!prod) return null return { preco_unitario: Number(prod.preco), preco_promocional: prod.preco_promocional ? Number(prod.preco_promocional) : null, nome_produto: prod.nome, sku: prod.sku, } } // Desconto por volume PJ (PROMO-05): >10 itens = 3%, >30 = 5%, >50 = 8% function descontoPorVolume(tipo, quantidade) { if (tipo !== 'PJ') return 0 if (quantidade >= 50) return 8 if (quantidade >= 30) return 5 if (quantidade >= 10) return 3 return 0 } const cotacaoController = { // ── Área do cliente ─────────────────────────────────────── async minhas(req, res) { try { const rows = await Cotacao.listByUser(req.user.id, req.tenant.id) res.json(rows) } catch (err) { res.status(500).json({ error: 'Erro ao listar cotações' }) } }, async getCotacao(req, res) { try { const cotacao = await Cotacao.findById(req.params.id) if (!cotacao) return res.status(404).json({ error: 'Cotação não encontrada' }) const itens = await Cotacao.listItens(req.params.id) res.json({ ...cotacao, itens }) } catch (err) { res.status(500).json({ error: 'Erro ao buscar cotação' }) } }, async criarOuObterRascunho(req, res) { try { const tenantId = req.tenant.id const userId = req.user.id // Busca rascunho aberto do usuário const [rascunho] = await Cotacao.listByUser(userId, tenantId) .then(rows => rows.filter(r => r.status === 'rascunho')) if (rascunho) { const itens = await Cotacao.listItens(rascunho.id) return res.json({ ...rascunho, itens }) } const nova = await Cotacao.create({ tenant_id: tenantId, user_id: userId }) res.status(201).json({ ...nova, itens: [] }) } catch (err) { console.error('[cotacao.rascunho]', err.message) res.status(500).json({ error: 'Erro ao criar carrinho' }) } }, async addItem(req, res) { try { const { cotacao_id, produto_id, quantidade } = req.body if (!cotacao_id || !produto_id || !quantidade) { return res.status(400).json({ error: 'cotacao_id, produto_id e quantidade são obrigatórios' }) } const cotacao = await Cotacao.findById(cotacao_id) if (!cotacao) return res.status(404).json({ error: 'Cotação não encontrada' }) const info = await resolverPreco(produto_id, req.tenant.id) if (!info) return res.status(404).json({ error: 'Produto não encontrado' }) const desconto_pct = descontoPorVolume(cotacao.cliente_tipo, quantidade) const item = await Cotacao.addItem({ cotacao_id, produto_id, ...info, quantidade, desconto_pct, }) await Cotacao.recalcTotal(cotacao_id) res.status(201).json(item) } catch (err) { console.error('[cotacao.addItem]', err.message) res.status(500).json({ error: 'Erro ao adicionar item' }) } }, async removeItem(req, res) { try { await Cotacao.removeItem(req.params.itemId) await Cotacao.recalcTotal(req.params.cotacaoId) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao remover item' }) } }, async updateItemObs(req, res) { try { const { itemId } = req.params const { observacao } = req.body await Cotacao.updateItemObs(itemId, observacao) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao salvar observação' }) } }, async salvar(req, res) { try { const { id } = req.params await Cotacao.update(id, { ...(await Cotacao.findById(id)), status: 'salva' }) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao salvar cotação' }) } }, async finalizar(req, res) { try { const { id } = req.params const { forma_pagto, endereco_id, tipo_entrega, observacao, endereco } = req.body if (!forma_pagto) return res.status(400).json({ error: 'forma_pagto é obrigatória' }) const cotacao = await Cotacao.findById(id) if (!cotacao) return res.status(404).json({ error: 'Cotação não encontrada' }) if (cotacao.status === 'finalizada') return res.status(409).json({ error: 'Cotação já finalizada' }) await Cotacao.update(id, { ...cotacao, forma_pagto }) const pedido = await Cotacao.toOrder(id, { endereco_id, tipo_entrega, observacao }) // Endereço inline (sem endereco_id salvo) if (endereco && tipo_entrega === 'entrega') { await Pedido.updateAddressInline(pedido.id, endereco) } // Pagamento na entrega → aguarda aprovação financeira if (forma_pagto === 'na_entrega') { const { execute } = require('../database/postgres') await execute( `UPDATE pedidos SET financeiro_status='pendente', updated_at=NOW() WHERE id=$1`, [pedido.id] ) } res.status(201).json(pedido) } catch (err) { console.error('[cotacao.finalizar]', err.message) res.status(500).json({ error: 'Erro ao finalizar cotação' }) } }, // ── Admin — financeiro ──────────────────────────────────── async listFinanceiro(req, res) { try { const { limit, offset } = req.query const rows = await Pedido.listFinanceiro(req.tenant.id, { limit, offset }) res.json(rows) } catch (err) { res.status(500).json({ error: 'Erro ao listar pendências financeiras' }) } }, async financeiroDecisao(req, res) { try { const { id } = req.params const { acao, obs } = req.body if (!['aprovar', 'rejeitar'].includes(acao)) { return res.status(400).json({ error: 'acao deve ser aprovar ou rejeitar' }) } const pedido = await Pedido.findById(id) if (!pedido) return res.status(404).json({ error: 'Pedido não encontrado' }) if (pedido.financeiro_status !== 'pendente') { return res.status(409).json({ error: 'Pedido já foi avaliado pelo financeiro' }) } await Pedido.financeiroDecisao(id, { acao, obs }) res.json({ ok: true, acao }) } catch (err) { console.error('[cotacao.financeiroDecisao]', err.message) res.status(500).json({ error: 'Erro ao processar decisão financeira' }) } }, // ── Admin — pedidos ─────────────────────────────────────── async listPedidos(req, res) { try { const { status, limit, offset } = req.query const rows = await Pedido.listAdmin(req.tenant.id, { status, limit, offset }) res.json(rows) } catch (err) { res.status(500).json({ error: 'Erro ao listar pedidos' }) } }, async getPedido(req, res) { try { const pedido = await Pedido.findById(req.params.id) if (!pedido) return res.status(404).json({ error: 'Pedido não encontrado' }) const [itens, historico_responsaveis] = await Promise.all([ Pedido.listItens(req.params.id), Pedido.findHistorico(req.params.id), ]) res.json({ ...pedido, itens, historico_responsaveis }) } catch (err) { res.status(500).json({ error: 'Erro ao buscar pedido' }) } }, async updateStatusPedido(req, res) { try { const { status, responsavel_entrega } = req.body const statusValidos = ['aguardando','separacao','embalagem','auditoria','fiscal','expedicao','entregue','cancelado'] if (!statusValidos.includes(status)) return res.status(400).json({ error: 'Status inválido' }) const { expedidor_id } = req.body const accountId = expedidor_id ? parseInt(expedidor_id) : (req.admin?.id || req.teamAccount?.accountId || null) if (status === 'entregue') { await Pedido.marcarEntregue(req.params.id, responsavel_entrega || null, accountId) } else { await Pedido.updateStatus(req.params.id, status, accountId) } // Notificação interna para o usuário (user_id) que fez o pedido const STATUS_NOTIF = { separacao: { title: '📦 Pedido em separação', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} está sendo preparado.` }, embalagem: { title: '📦 Pedido em embalagem', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} está sendo embalado.` }, expedicao: { title: '🚚 Pedido saiu para entrega', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} saiu para entrega. Fique de olho!` }, entregue: { title: '✅ Pedido marcado como entregue', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} foi marcado como entregue. Se não recebeu, entre em contato.` }, cancelado: { title: '❌ Pedido cancelado', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} foi cancelado. Fale conosco se precisar de ajuda.` }, auditoria: { title: '🔍 Pedido em auditoria', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} está em verificação final.` }, fiscal: { title: '🧾 Pedido em processamento fiscal', message: (p) => `Seu pedido ${p.protocolo || '#'+p.id} está na etapa fiscal.` }, } if (STATUS_NOTIF[status]) { setImmediate(async () => { try { const pedido = await Pedido.findById(req.params.id) if (!pedido?.user_id) return const userRow = await dbQueryOne('SELECT cpf FROM users WHERE id = $1', [pedido.user_id]) if (!userRow?.cpf) return const notif = STATUS_NOTIF[status] await Notification.create({ user_cpf: userRow.cpf.replace(/\D/g, ''), title: notif.title, message: notif.message(pedido), type: status === 'cancelado' ? 'error' : status === 'entregue' ? 'success' : 'info', }) } catch { /* silent */ } }) } // WPP-08 — notifica cliente por WhatsApp em mudanças de status relevantes const STATUS_MSG = { separacao: (p) => `📦 *Pedido #${p.id}${p.protocolo ? ` (${p.protocolo})` : ''} em separação!*\nEstamos preparando seu pedido. Em breve está pronto! 😊`, expedicao: (p) => `🚚 *Pedido #${p.id}${p.protocolo ? ` (${p.protocolo})` : ''} saiu para entrega!*\nEm breve chegará até você. Qualquer dúvida, fale conosco! 🛵`, entregue: (p) => `✅ *Pedido #${p.id} entregue com sucesso!*\nObrigado pela confiança! Ficamos felizes em servir você. ⭐`, cancelado: (p) => `❌ *Pedido #${p.id} cancelado.*\nPrecisa de ajuda? Fale conosco aqui pelo WhatsApp.`, } if (STATUS_MSG[status]) { setImmediate(async () => { try { const pedido = await Pedido.findById(req.params.id) if (!pedido?.cliente_id) return const { query: dbQuery } = require('../database/postgres') const tels = await dbQuery( `SELECT numero FROM cliente_telefones WHERE cliente_id = $1 AND aceita_whatsapp = true ORDER BY principal DESC LIMIT 1`, [pedido.cliente_id] ) if (!tels.length) return const numero = tels[0].numero.replace(/\D/g, '') const msg = STATUS_MSG[status](pedido) const apiUrl = process.env.NEWWHATS_API_URL if (!apiUrl) return await fetch(`${apiUrl}/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.NEWWHATS_API_KEY || ''}` }, body: JSON.stringify({ phone: `55${numero}`, message: msg }), }).catch(() => {}) } catch { /* silent — notificação não deve travar o fluxo */ } }) } // Notifica WA-Inbox (best-effort) if (accountId) setImmediate(() => notifyNwAssignAgent(req.params.id, accountId)) res.json({ ok: true }) } catch (err) { console.error('[pedido.updateStatus]', err.message) res.status(500).json({ error: 'Erro ao atualizar status' }) } }, // EMB-02 — registra embalador; aceita embalador_id (account FK) ou texto legado async marcarEmbalado(req, res) { try { const { embalador, embalador_id } = req.body if (!embalador && !embalador_id) return res.status(400).json({ error: 'embalador ou embalador_id é obrigatório' }) // accountId: preferência para embalador_id explícito, depois token do colaborador logado const accountId = embalador_id ? parseInt(embalador_id) : (req.admin?.id || req.teamAccount?.accountId || null) await Pedido.marcarEmbalado(req.params.id, embalador || null, accountId) if (accountId) setImmediate(() => notifyNwAssignAgent(req.params.id, accountId)) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao registrar embalagem' }) } }, // Lista colaboradores disponíveis para uma função operacional. A regra é // direta: o colaborador aparece se está vinculado (team_members) a um setor // cujo nome bate com a função. Cada função aceita variações de nome para // tolerar grafias comuns ("Separação"/"Separacao"/"Separar"). Setor é a fonte // da verdade — gerenciado em /admin-v2/equipe; nada para configurar em Settings. async colaboradoresDisponiveis(req, res) { try { const SETOR_POR_FUNCAO = { separador: ['separação','separacao','separar','separador'], embalador: ['embalagem','embalador','embalar'], auditor: ['auditoria','auditor','auditar'], fiscal: ['fiscal','fiscalização','fiscalizacao'], expedidor: ['expedição','expedicao','entrega','expedidor','motoboy','entregador'], } const funcao = String(req.query.funcao || '').toLowerCase() const aliases = SETOR_POR_FUNCAO[funcao] if (!aliases) { return res.status(400).json({ error: 'Função inválida', funcoes_validas: Object.keys(SETOR_POR_FUNCAO) }) } const tenantId = req.tenant.id // Coluna de FK no pedido para contar pedidos ativos por colaborador const FK = { separador: 'separador_id', embalador: 'embalador_id', auditor: 'auditor_id', fiscal: 'fiscal_id', expedidor: 'expedidor_id', }[funcao] // Match case-insensitive contra os aliases do setor. // DISTINCT ON evita duplicar colaborador que esteja em mais de um setor compatível. const colaboradores = await dbQuery(` SELECT DISTINCT ON (a.id) a.id, a.name, a.email, a.role, a.availability, a.avatar_url, a.phone_connected, COALESCE(BOOL_OR(tm.on_duty) OVER (PARTITION BY a.id), FALSE) AS on_duty, s.name AS sector_match, (SELECT COUNT(*)::int FROM pedidos p WHERE p.tenant_id = $1 AND p.${FK} = a.id AND p.status NOT IN ('entregue','cancelado')) AS pedidos_ativos, (SELECT MAX(created_at) FROM pedidos p WHERE p.tenant_id = $1 AND p.${FK} = a.id) AS ultimo_pedido_em FROM accounts a JOIN team_members tm ON tm.account_id = a.id JOIN sectors s ON s.id = tm.sector_id WHERE a.tenant_id = $1 AND a.active = TRUE AND s.active = TRUE AND LOWER(s.name) = ANY($2::text[]) ORDER BY a.id, a.name `, [tenantId, aliases]) // Reordena no Node (simples e legível) — disponível primeiro, depois ocupado, offline por último. colaboradores.sort((x, y) => { const r = (a) => a.availability === 'online' ? 0 : a.availability === 'busy' ? 1 : 2 const d = r(x) - r(y) return d !== 0 ? d : x.name.localeCompare(y.name) }) res.json({ configurado: true, setores_aceitos: aliases, colaboradores, }) } catch (err) { console.error('[colaboradoresDisponiveis]', err.message) res.status(500).json({ error: 'Erro ao listar colaboradores' }) } }, // Endpoint genérico para registrar separador/auditor/fiscal num pedido. // Embalagem (mudança de status) usa marcarEmbalado; expedição usa marcarEntregue. async setResponsavelEtapa(req, res) { try { const FUNCOES_VALIDAS = ['separador','auditor','fiscal'] const funcao = String(req.params.funcao || '').toLowerCase() if (!FUNCOES_VALIDAS.includes(funcao)) { return res.status(400).json({ error: 'Função inválida' }) } const accountId = req.body.account_id ? parseInt(req.body.account_id) : null if (!accountId) return res.status(400).json({ error: 'account_id é obrigatório' }) // Valida que o colaborador pertence ao tenant antes de gravar const acc = await dbQueryOne( `SELECT id FROM accounts WHERE id=$1 AND tenant_id=$2 AND active=TRUE`, [accountId, req.tenant.id] ) if (!acc) return res.status(404).json({ error: 'Colaborador não encontrado' }) await Pedido.setResponsavel(req.params.id, funcao, accountId) setImmediate(() => notifyNwAssignAgent(req.params.id, accountId)) res.json({ ok: true }) } catch (err) { console.error('[setResponsavelEtapa]', err.message) res.status(500).json({ error: 'Erro ao atribuir responsável' }) } }, // Registra cupom fiscal emitido externamente (PDV/ECF). // Aceita em qualquer fase pós-pagamento — validação leve para não bloquear // workflow real do operador. Pode ser sobrescrito (re-emissão por erro). async registrarCupomFiscal(req, res) { try { const id = req.params.id const { numero, chave } = req.body || {} const num = String(numero || '').trim() const chave44 = String(chave || '').replace(/\D/g, '') if (!num && !chave44) { return res.status(400).json({ error: 'Informe o número do cupom ou a chave de acesso (44 dígitos).' }) } if (chave44 && chave44.length !== 44) { return res.status(400).json({ error: 'Chave de acesso NFC-e deve ter 44 dígitos.' }) } const pedido = await Pedido.findById(id) if (!pedido) return res.status(404).json({ error: 'Pedido não encontrado' }) if (pedido.tenant_id !== req.tenant.id) return res.status(403).json({ error: 'Acesso negado' }) if (['cancelado'].includes(pedido.status)) { return res.status(409).json({ error: 'Pedido cancelado — não é possível emitir cupom.' }) } const accountId = req.admin?.id || req.teamAccount?.accountId || null await Pedido.registrarCupom(id, { numero: num || null, chave: chave44 || null, accountId }) const atualizado = await Pedido.findById(id) res.json({ ok: true, cupom_numero: atualizado.cupom_numero, cupom_chave: atualizado.cupom_chave, cupom_emitido_em: atualizado.cupom_emitido_em, cupom_emitido_por: atualizado.cupom_emitido_por, cupom_emitido_por_nome: atualizado.cupom_emitido_por_nome, }) } catch (err) { console.error('[registrarCupomFiscal]', err.message) res.status(500).json({ error: 'Erro ao registrar cupom fiscal' }) } }, async atribuirEquipe(req, res) { try { await Pedido.atribuirEquipe(req.params.id, req.body.equipe_id) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao atribuir equipe' }) } }, async marcarItemSeparado(req, res) { try { await Pedido.marcarItemSeparado(req.params.itemId, req.body.separado !== false) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao marcar item' }) } }, async setAlertaItem(req, res) { try { const { tipo, obs, foto } = req.body const TIPOS = ['falta', 'marca_diferente', 'substituicao'] if (!TIPOS.includes(tipo)) return res.status(400).json({ error: 'tipo inválido: falta | marca_diferente | substituicao' }) await Pedido.setAlertaItem(req.params.itemId, { tipo, obs, foto }) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao salvar alerta' }) } }, async limparAlertaItem(req, res) { try { await Pedido.limparAlertaItem(req.params.itemId) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao limpar alerta' }) } }, async confirmarAlertaItem(req, res) { try { await Pedido.confirmarAlertaItem(req.params.itemId) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao confirmar alerta' }) } }, // DASH-04 — notificações internas de novos pedidos async notificacoes(req, res) { try { const minutes = Math.min(Number(req.query.minutes) || 1440, 10080) // max 7 dias const rows = await Pedido.notificacoes(req.tenant.id, minutes) res.json(rows) } catch (err) { res.status(500).json({ error: 'Erro ao buscar notificações' }) } }, async contadores(req, res) { try { const dados = await Pedido.contadores(req.tenant.id) // Promoções ativas (PROMO-09) const promos = await Promotion.findAllActive(req.tenant.id) res.json({ ...dados, promocoes_ativas: promos.length }) } catch (err) { res.status(500).json({ error: 'Erro ao buscar contadores' }) } }, // PROMO-10 — Flash promo ativa para o checkout da cotação async getFlashPromo(req, res) { try { const promo = await CotacaoFlashPromo.getActive(req.tenant?.id ?? 1) res.json(promo ?? null) } catch (err) { res.status(500).json({ error: 'Erro ao buscar flash promo' }) } }, async createFlashPromo(req, res) { try { const { produto_id, discount_pct, label, duration_minutes } = req.body if (!discount_pct || !duration_minutes) { return res.status(400).json({ error: 'discount_pct e duration_minutes são obrigatórios' }) } const result = await CotacaoFlashPromo.create({ tenantId: req.tenant?.id ?? 1, produtoId: produto_id || null, discount_pct, label, duration_minutes, }) res.status(201).json(result) } catch (err) { res.status(500).json({ error: 'Erro ao criar flash promo' }) } }, async cancelFlashPromo(req, res) { try { await CotacaoFlashPromo.cancel(req.params.id) res.json({ ok: true }) } catch (err) { res.status(500).json({ error: 'Erro ao cancelar flash promo' }) } }, // FUT-04/05 — Ranking de equipes e relatório de performance async rankingEquipes(req, res) { try { const { query } = require('../database/postgres') const { from, to } = req.query const params = [req.tenant.id] let where = 'WHERE p.tenant_id = $1' if (from) { params.push(from); where += ` AND DATE(p.created_at) >= $${params.length}` } if (to) { params.push(to); where += ` AND DATE(p.created_at) <= $${params.length}` } const ranking = await query( `SELECT p.equipe_id, COUNT(*) FILTER (WHERE p.status = 'entregue') AS pedidos_entregues, COUNT(*) FILTER (WHERE p.status NOT IN ('cancelado','aguardando')) AS pedidos_processados, ROUND(AVG(EXTRACT(EPOCH FROM (p.entregue_em - p.created_at))/3600)::NUMERIC, 1) AS media_horas_entrega, COUNT(*) FILTER (WHERE p.embalador IS NOT NULL) AS pedidos_embalados, MIN(p.created_at)::DATE AS primeiro_pedido, MAX(p.created_at)::DATE AS ultimo_pedido FROM pedidos p ${where} AND p.equipe_id IS NOT NULL GROUP BY p.equipe_id ORDER BY pedidos_entregues DESC`, params ) res.json(ranking) } catch (err) { res.status(500).json({ error: 'Erro ao buscar ranking' }) } }, async relatorioSetor(req, res) { try { const { query } = require('../database/postgres') const { from, to } = req.query const params = [req.tenant.id] let where = 'WHERE tenant_id = $1' if (from) { params.push(from); where += ` AND DATE(created_at) >= $${params.length}` } if (to) { params.push(to); where += ` AND DATE(created_at) <= $${params.length}` } const [volumeStatus, volumeDia, top_total] = await Promise.all([ query( `SELECT status, COUNT(*) AS total, SUM(total) AS receita FROM pedidos ${where} GROUP BY status ORDER BY total DESC`, params ), query( `SELECT DATE(created_at) AS dia, COUNT(*) AS pedidos, SUM(total) AS receita FROM pedidos ${where} GROUP BY DATE(created_at) ORDER BY dia DESC LIMIT 30`, params ), query( `SELECT cl.nome AS cliente, COUNT(*) AS pedidos, SUM(p.total) AS gasto FROM pedidos p LEFT JOIN clientes cl ON cl.id = p.cliente_id ${where} AND p.status = 'entregue' GROUP BY cl.nome ORDER BY gasto DESC LIMIT 10`, params ), ]) res.json({ volume_por_status: volumeStatus, volume_por_dia: volumeDia, top_clientes: top_total }) } catch (err) { res.status(500).json({ error: 'Erro ao buscar relatório' }) } }, } module.exports = cotacaoController