Files
mercado.clube67.com/backend/models/Pedido.js
T

274 lines
11 KiB
JavaScript

'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