274 lines
11 KiB
JavaScript
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
|