'use strict' const { execute, queryOne, query } = require('../database/postgres') class Pedido { static findById(id) { return queryOne(` SELECT p.*, COALESCE(cl.nome, u.nome) AS cliente_nome, COALESCE(cl.tipo, 'PF') AS cliente_tipo, COALESCE(cl.cpf_cnpj, u.cpf) AS cpf_cnpj, COALESCE(cl.email, u.email) AS cliente_email, COALESCE(e.rua, p.end_rua) AS rua, COALESCE(e.numero, p.end_numero) AS end_numero, COALESCE(e.bairro, p.end_bairro) AS bairro, COALESCE(e.cidade, p.end_cidade) AS cidade, COALESCE(e.estado, p.end_estado) AS estado, COALESCE(e.cep, p.end_cep) AS cep, ase.name AS separador_nome, aem.name AS embalador_nome, aau.name AS auditor_nome, afi.name AS fiscal_nome, aex.name AS expedidor_nome, acu.name AS cupom_emitido_por_nome FROM pedidos p LEFT JOIN clientes cl ON cl.id = p.cliente_id LEFT JOIN users u ON u.id = p.user_id LEFT JOIN cliente_enderecos e ON e.id = p.endereco_id LEFT JOIN accounts ase ON ase.id = p.separador_id LEFT JOIN accounts aem ON aem.id = p.embalador_id LEFT JOIN accounts aau ON aau.id = p.auditor_id LEFT JOIN accounts afi ON afi.id = p.fiscal_id LEFT JOIN accounts aex ON aex.id = p.expedidor_id LEFT JOIN accounts acu ON acu.id = p.cupom_emitido_por WHERE p.id = $1 `, [id]) } static listFinanceiro(tenantId, { limit = 50, offset = 0 } = {}) { return query( `SELECT p.*, COALESCE(cl.nome, u.nome) AS cliente_nome, COALESCE(cl.tipo, 'PF') AS cliente_tipo, COALESCE(cl.cpf_cnpj, u.cpf) AS cpf_cnpj FROM pedidos p LEFT JOIN clientes cl ON cl.id = p.cliente_id LEFT JOIN users u ON u.id = p.user_id WHERE p.tenant_id = $1 AND p.financeiro_status = 'pendente' ORDER BY p.created_at ASC LIMIT $2 OFFSET $3`, [tenantId, Math.min(Number(limit) || 50, 200), Math.max(Number(offset) || 0, 0)] ) } static financeiroDecisao(id, { acao, obs }) { if (acao === 'aprovar') { return execute( `UPDATE pedidos SET financeiro_status='aprovado', financeiro_obs=$1, financeiro_em=NOW(), updated_at=NOW() WHERE id=$2`, [obs || null, id] ) } // rejeitar → cancela o pedido return execute( `UPDATE pedidos SET financeiro_status='rejeitado', financeiro_obs=$1, financeiro_em=NOW(), status='cancelado', updated_at=NOW() WHERE id=$2`, [obs || null, id] ) } static updateAddressInline(id, { cep, rua, numero, complemento, bairro, cidade, estado }) { return execute( `UPDATE pedidos SET end_cep=$1, end_rua=$2, end_numero=$3, end_complemento=$4, end_bairro=$5, end_cidade=$6, end_estado=$7, updated_at=NOW() WHERE id=$8`, [cep, rua, numero, complemento, bairro, cidade, estado, id] ) } static listAdmin(tenantId, { status, from, to, limit = 50, offset = 0 } = {}) { const params = [tenantId] let where = 'WHERE p.tenant_id = $1' if (status) { params.push(status); where += ` AND p.status = $${params.length}` } 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}` } params.push(Math.min(Number(limit) || 50, 200)) params.push(Math.max(Number(offset) || 0, 0)) return query( `SELECT p.*, COALESCE(cl.nome, u.nome) AS cliente_nome, COALESCE(cl.tipo, 'PF') AS cliente_tipo FROM pedidos p LEFT JOIN clientes cl ON cl.id = p.cliente_id LEFT JOIN users u ON u.id = p.user_id ${where} ORDER BY p.created_at DESC LIMIT $${params.length - 1} OFFSET $${params.length}`, params ) } static listByCliente(clienteId, tenantId) { return query( `SELECT * FROM pedidos WHERE cliente_id = $1 AND tenant_id = $2 ORDER BY created_at DESC`, [clienteId, tenantId] ) } static listItens(pedidoId) { return query( `SELECT pi.*, p.imagem AS produto_imagem FROM pedido_itens pi LEFT JOIN produtos p ON p.id = pi.produto_id WHERE pi.pedido_id = $1 ORDER BY pi.id ASC`, [pedidoId] ) } // Retorna o log de quem executou cada etapa, com dados do colaborador static findHistorico(pedidoId) { return query(` SELECT psl.id, psl.status, psl.created_at AS feito_em, a.id AS account_id, a.name AS account_name, a.avatar_url AS account_avatar, a.role AS account_role FROM pedido_status_log psl LEFT JOIN accounts a ON a.id = psl.account_id WHERE psl.pedido_id = $1 ORDER BY psl.created_at ASC `, [pedidoId]) } static async logStatus(pedidoId, status, accountId = null) { try { await execute( 'INSERT INTO pedido_status_log (pedido_id, status, account_id) VALUES ($1,$2,$3)', [pedidoId, status, accountId || null] ) } catch { /* nunca bloqueia o fluxo principal */ } } static async updateStatus(id, status, accountId = null) { // Quando entra em expedição, registra o horário de saída do entregador. // Só seta uma vez (COALESCE preserva valor já existente em re-transições). if (status === 'expedicao') { await execute( `UPDATE pedidos SET status=$1, expedidor_saida_em = COALESCE(expedidor_saida_em, NOW()), expedidor_id = COALESCE($2, expedidor_id), updated_at = NOW() WHERE id=$3`, [status, accountId || null, id] ) } else { await execute('UPDATE pedidos SET status=$1, updated_at=NOW() WHERE id=$2', [status, id]) } await Pedido.logStatus(id, status, accountId) } // Registra cupom fiscal emitido externamente (PDV/ECF). // numero: COO ou número do cupom. chave: chave de acesso NFC-e (44 dígitos). // accountId: quem clicou no botão (auditoria). static async registrarCupom(id, { numero, chave, accountId }) { await execute( `UPDATE pedidos SET cupom_numero = $1, cupom_chave = $2, cupom_emitido_em = NOW(), cupom_emitido_por = $3, updated_at = NOW() WHERE id = $4`, [numero || null, chave || null, accountId || null, id] ) } static atribuirEquipe(id, equipeId) { return execute('UPDATE pedidos SET equipe_id=$1, updated_at=NOW() WHERE id=$2', [equipeId, id]) } static async marcarEntregue(id, responsavel, accountId = null) { const expedidorId = accountId || null await execute( `UPDATE pedidos SET status=$1, responsavel_entrega=$2, expedidor_id=$3, entregue_em=NOW(), updated_at=NOW() WHERE id=$4`, ['entregue', responsavel || null, expedidorId, id] ) await Pedido.logStatus(id, 'entregue', accountId) } static marcarItemSeparado(itemId, separado) { return execute('UPDATE pedido_itens SET separado=$1 WHERE id=$2', [separado, itemId]) } static setAlertaItem(itemId, { tipo, obs, foto }) { return execute( `UPDATE pedido_itens SET alerta_tipo=$1, alerta_obs=$2, alerta_foto=$3, alerta_confirmado=FALSE, alerta_em=NOW() WHERE id=$4`, [tipo || null, obs || null, foto || null, itemId] ) } static confirmarAlertaItem(itemId) { return execute( `UPDATE pedido_itens SET alerta_confirmado=TRUE WHERE id=$1`, [itemId] ) } static limparAlertaItem(itemId) { return execute( `UPDATE pedido_itens SET alerta_tipo=NULL, alerta_obs=NULL, alerta_foto=NULL, alerta_confirmado=FALSE, alerta_em=NULL WHERE id=$1`, [itemId] ) } // EMB-02 — registra embalador (texto legado) + embalador_id (FK) e timestamp static async marcarEmbalado(id, embalador, accountId = null) { // embalador_id prevalece sobre texto livre; accountId é o id do colaborador logado const embaladorId = accountId || null await execute( `UPDATE pedidos SET status=$1, embalador=$2, embalador_id=$3, embalado_em=NOW(), updated_at=NOW() WHERE id=$4`, ['embalagem', embalador || null, embaladorId, id] ) await Pedido.logStatus(id, 'embalagem', accountId) } // Atribui um colaborador como responsável por uma etapa (separador/auditor/fiscal). // Para embalador/expedidor existem métodos dedicados (marcarEmbalado/marcarEntregue) // por terem efeitos colaterais — mudança de status, timestamps e notificações. // Mapeia funcao → coluna de FK; rejeita funções desconhecidas (segurança contra SQLi). static async setResponsavel(id, funcao, accountId) { const COL = { separador: 'separador_id', auditor: 'auditor_id', fiscal: 'fiscal_id' } const col = COL[funcao] if (!col) throw new Error(`Função inválida: ${funcao}`) await execute(`UPDATE pedidos SET ${col}=$1, updated_at=NOW() WHERE id=$2`, [accountId || null, id]) } // DASH-04 — notificações internas (novos pedidos/cotações recentes) static notificacoes(tenantId, limitMinutes = 1440) { return query(` SELECT id, status, total, cliente_id, created_at, 'pedido' AS tipo FROM pedidos WHERE tenant_id = $1 AND created_at >= NOW() - ($2 || ' minutes')::INTERVAL ORDER BY created_at DESC LIMIT 30 `, [tenantId, limitMinutes]) } // Contadores para dashboard static contadores(tenantId) { return queryOne(` SELECT COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS hoje, COUNT(*) FILTER (WHERE status = 'aguardando') AS aguardando, COUNT(*) FILTER (WHERE status = 'separacao') AS em_separacao, COUNT(*) FILTER (WHERE status = 'embalagem') AS em_embalagem, COUNT(*) FILTER (WHERE status = 'auditoria') AS em_auditoria, COUNT(*) FILTER (WHERE status IN ('fiscal','expedicao')) AS prontos, COUNT(*) FILTER (WHERE status = 'entregue') AS entregues, COUNT(*) FILTER (WHERE status NOT IN ('entregue','cancelado')) AS em_aberto, COUNT(*) FILTER (WHERE financeiro_status = 'pendente') AS financeiro_pendente FROM pedidos WHERE tenant_id = $1 `, [tenantId]) } } module.exports = Pedido