feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
'use strict'
|
||||
|
||||
const { execute, queryOne, query } = require('../database/postgres')
|
||||
|
||||
class Cotacao {
|
||||
static findById(id) {
|
||||
return queryOne(`
|
||||
SELECT c.*,
|
||||
COALESCE(cl.nome, u.nome) AS cliente_nome,
|
||||
COALESCE(cl.tipo, 'PF') AS cliente_tipo
|
||||
FROM cotacoes c
|
||||
LEFT JOIN clientes cl ON cl.id = c.cliente_id
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE c.id = $1
|
||||
`, [id])
|
||||
}
|
||||
|
||||
static listByCliente(clienteId, tenantId) {
|
||||
return query(
|
||||
`SELECT * FROM cotacoes WHERE cliente_id = $1 AND tenant_id = $2 ORDER BY created_at DESC`,
|
||||
[clienteId, tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
static listByUser(userId, tenantId) {
|
||||
return query(
|
||||
`SELECT DISTINCT ON (c.id) c.*,
|
||||
p.id AS pedido_id,
|
||||
p.protocolo AS pedido_protocolo,
|
||||
p.status AS pedido_status,
|
||||
p.forma_pagto AS pedido_forma_pagto,
|
||||
p.tipo_entrega AS pedido_tipo_entrega,
|
||||
p.financeiro_status AS pedido_financeiro_status,
|
||||
(
|
||||
SELECT json_agg(json_build_object(
|
||||
'status', sl.status,
|
||||
'created_at', sl.created_at
|
||||
) ORDER BY sl.created_at ASC)
|
||||
FROM pedido_status_log sl
|
||||
WHERE sl.pedido_id = p.id
|
||||
) AS pedido_status_log,
|
||||
(
|
||||
SELECT json_agg(json_build_object(
|
||||
'id', pi.id,
|
||||
'nome_produto', pi.nome_produto,
|
||||
'sku', pi.sku,
|
||||
'quantidade', pi.quantidade,
|
||||
'subtotal', pi.subtotal,
|
||||
'observacao', pi.observacao,
|
||||
'alerta_tipo', pi.alerta_tipo,
|
||||
'alerta_obs', pi.alerta_obs,
|
||||
'alerta_foto', pi.alerta_foto,
|
||||
'alerta_confirmado',pi.alerta_confirmado,
|
||||
'alerta_em', pi.alerta_em
|
||||
) ORDER BY pi.id ASC)
|
||||
FROM pedido_itens pi
|
||||
WHERE pi.pedido_id = p.id
|
||||
) AS itens
|
||||
FROM cotacoes c
|
||||
LEFT JOIN pedidos p ON p.cotacao_id = c.id AND p.tenant_id = c.tenant_id
|
||||
WHERE c.user_id = $1 AND c.tenant_id = $2
|
||||
ORDER BY c.id DESC, p.id DESC
|
||||
LIMIT 50`,
|
||||
[userId, tenantId]
|
||||
)
|
||||
}
|
||||
|
||||
static listAdmin(tenantId, { status, limit = 50, offset = 0 } = {}) {
|
||||
const params = [tenantId]
|
||||
let where = 'WHERE c.tenant_id = $1'
|
||||
if (status) { params.push(status); where += ` AND c.status = $${params.length}` }
|
||||
params.push(Math.min(Number(limit) || 50, 200))
|
||||
params.push(Math.max(Number(offset) || 0, 0))
|
||||
return query(
|
||||
`SELECT c.*,
|
||||
COALESCE(cl.nome, u.nome) AS cliente_nome,
|
||||
COALESCE(cl.tipo, 'PF') AS cliente_tipo
|
||||
FROM cotacoes c
|
||||
LEFT JOIN clientes cl ON cl.id = c.cliente_id
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
${where} ORDER BY c.created_at DESC
|
||||
LIMIT $${params.length - 1} OFFSET $${params.length}`,
|
||||
params
|
||||
)
|
||||
}
|
||||
|
||||
static create({ tenant_id, cliente_id, user_id, status = 'rascunho', forma_pagto, total, desconto, observacao }) {
|
||||
return execute(
|
||||
`INSERT INTO cotacoes (tenant_id,cliente_id,user_id,status,forma_pagto,total,desconto,observacao)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
||||
[tenant_id, cliente_id || null, user_id || null, status,
|
||||
forma_pagto || null, total || 0, desconto || 0, observacao || null]
|
||||
)
|
||||
}
|
||||
|
||||
static update(id, { status, forma_pagto, total, desconto, observacao }) {
|
||||
return execute(
|
||||
`UPDATE cotacoes SET status=$1,forma_pagto=$2,total=$3,desconto=$4,observacao=$5,updated_at=NOW()
|
||||
WHERE id=$6`,
|
||||
[status, forma_pagto || null, total, desconto || 0, observacao || null, id]
|
||||
)
|
||||
}
|
||||
|
||||
// ── Itens ─────────────────────────────────────────────────
|
||||
|
||||
static listItens(cotacaoId) {
|
||||
return query(
|
||||
`SELECT ci.*, p.imagem AS produto_imagem
|
||||
FROM cotacao_itens ci
|
||||
LEFT JOIN produtos p ON p.id = ci.produto_id
|
||||
WHERE ci.cotacao_id = $1 ORDER BY ci.id ASC`,
|
||||
[cotacaoId]
|
||||
)
|
||||
}
|
||||
|
||||
static addItem({ cotacao_id, produto_id, nome_produto, sku, quantidade, preco_unitario, preco_promocional, desconto_pct, observacao }) {
|
||||
const precoFinal = preco_promocional || preco_unitario
|
||||
const subtotal = precoFinal * quantidade * (1 - (desconto_pct || 0) / 100)
|
||||
return execute(
|
||||
`INSERT INTO cotacao_itens
|
||||
(cotacao_id,produto_id,nome_produto,sku,quantidade,preco_unitario,preco_promocional,desconto_pct,subtotal,observacao)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`,
|
||||
[cotacao_id, produto_id || null, nome_produto, sku || null,
|
||||
quantidade, preco_unitario, preco_promocional || null, desconto_pct || 0, subtotal, observacao || null]
|
||||
)
|
||||
}
|
||||
|
||||
static updateItemObs(itemId, observacao) {
|
||||
return execute(
|
||||
`UPDATE cotacao_itens SET observacao=$1 WHERE id=$2`,
|
||||
[observacao || null, itemId]
|
||||
)
|
||||
}
|
||||
|
||||
static removeItem(itemId) {
|
||||
return execute('DELETE FROM cotacao_itens WHERE id = $1', [itemId])
|
||||
}
|
||||
|
||||
static clearItens(cotacaoId) {
|
||||
return execute('DELETE FROM cotacao_itens WHERE cotacao_id = $1', [cotacaoId])
|
||||
}
|
||||
|
||||
static async recalcTotal(cotacaoId) {
|
||||
await execute(
|
||||
`UPDATE cotacoes SET total = (
|
||||
SELECT COALESCE(SUM(subtotal),0) FROM cotacao_itens WHERE cotacao_id = $1
|
||||
), updated_at = NOW() WHERE id = $1`,
|
||||
[cotacaoId]
|
||||
)
|
||||
}
|
||||
|
||||
// ── Converter cotação em pedido ───────────────────────────
|
||||
|
||||
static async toOrder(cotacaoId, { endereco_id, tipo_entrega, observacao } = {}) {
|
||||
const cotacao = await Cotacao.findById(cotacaoId)
|
||||
if (!cotacao) throw new Error('Cotação não encontrada')
|
||||
const itens = await Cotacao.listItens(cotacaoId)
|
||||
|
||||
// Strategy 2 — se veio do portal (user_id preenchido mas cliente_id nulo),
|
||||
// garante um registro em clientes para manter rastreabilidade no admin
|
||||
let clienteId = cotacao.cliente_id
|
||||
if (!clienteId && cotacao.user_id) {
|
||||
// verifica se já existe um clientes vinculado a este user_id
|
||||
const existing = await queryOne(
|
||||
`SELECT id FROM clientes WHERE user_id = $1 AND tenant_id = $2 LIMIT 1`,
|
||||
[cotacao.user_id, cotacao.tenant_id]
|
||||
)
|
||||
if (existing) {
|
||||
clienteId = existing.id
|
||||
} else {
|
||||
// cria o registro usando dados do users
|
||||
const user = await queryOne(`SELECT * FROM users WHERE id = $1`, [cotacao.user_id])
|
||||
if (user) {
|
||||
const novo = await execute(
|
||||
`INSERT INTO clientes (tenant_id, user_id, nome, email, cpf_cnpj, tipo, created_at, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,'PF',NOW(),NOW()) RETURNING id`,
|
||||
[cotacao.tenant_id, user.id, user.nome, user.email || null, user.cpf || null]
|
||||
)
|
||||
clienteId = novo.rows[0]?.id || null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pedidoResult = await execute(
|
||||
`INSERT INTO pedidos
|
||||
(tenant_id,cotacao_id,cliente_id,user_id,endereco_id,status,forma_pagto,tipo_entrega,total,desconto,observacao)
|
||||
VALUES ($1,$2,$3,$4,$5,'aguardando',$6,$7,$8,$9,$10) RETURNING *`,
|
||||
[cotacao.tenant_id, cotacaoId, clienteId, cotacao.user_id,
|
||||
endereco_id || null, cotacao.forma_pagto, tipo_entrega || 'retirada',
|
||||
cotacao.total, cotacao.desconto, observacao || cotacao.observacao]
|
||||
)
|
||||
const pedido = pedidoResult.rows[0]
|
||||
if (!pedido) throw new Error('Falha ao criar pedido')
|
||||
|
||||
for (const item of itens) {
|
||||
await execute(
|
||||
`INSERT INTO pedido_itens (pedido_id,produto_id,nome_produto,sku,quantidade,preco_unitario,desconto_pct,subtotal,observacao)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
|
||||
[pedido.id, item.produto_id, item.nome_produto, item.sku,
|
||||
item.quantidade, item.preco_unitario, item.desconto_pct, item.subtotal, item.observacao || null]
|
||||
)
|
||||
}
|
||||
|
||||
// WPP-10 — gera protocolo único por pedido
|
||||
const dt = new Date()
|
||||
const yyyymmdd = `${dt.getFullYear()}${String(dt.getMonth()+1).padStart(2,'0')}${String(dt.getDate()).padStart(2,'0')}`
|
||||
const protocolo = `PED-${yyyymmdd}-${pedido.id}`
|
||||
await execute(`UPDATE pedidos SET protocolo=$1 WHERE id=$2`, [protocolo, pedido.id])
|
||||
|
||||
await execute(
|
||||
`UPDATE cotacoes SET status='finalizada', updated_at=NOW() WHERE id=$1`,
|
||||
[cotacaoId]
|
||||
)
|
||||
return { ...pedido, protocolo }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Cotacao
|
||||
Reference in New Issue
Block a user