346 lines
12 KiB
JavaScript
346 lines
12 KiB
JavaScript
'use strict'
|
||
|
||
const { Router } = require('express')
|
||
const { query, queryOne } = require('../../database/postgres')
|
||
const { nwAuth } = require('./auth')
|
||
|
||
function cleanPhone(tel) {
|
||
return String(tel || '').replace(/\D/g, '')
|
||
}
|
||
function cleanCpf(cpf) {
|
||
return String(cpf || '').replace(/\D/g, '')
|
||
}
|
||
|
||
function createRoutes() {
|
||
const router = Router()
|
||
router.use(nwAuth)
|
||
|
||
// ── GET /nw/ping ──────────────────────────────────────────────────────────
|
||
router.get('/ping', (_req, res) => {
|
||
res.json({ ok: true, system: 'alemao-conveniencias', ts: new Date().toISOString() })
|
||
})
|
||
|
||
// ── GET /nw/cliente?telefone= ─────────────────────────────────────────────
|
||
// Retorna dados do cliente + tickets da rifa ativa + clube ativo + candidaturas
|
||
router.get('/cliente', async (req, res) => {
|
||
const { telefone } = req.query
|
||
if (!telefone) return res.status(400).json({ error: '"telefone" é obrigatório' })
|
||
|
||
const clean = cleanPhone(telefone)
|
||
|
||
try {
|
||
const user = await queryOne(
|
||
`SELECT id, nome, telefone, cpf, email, timestamp AS created_at
|
||
FROM users
|
||
WHERE REGEXP_REPLACE(telefone, '\\D', '', 'g') = $1`,
|
||
[clean]
|
||
)
|
||
|
||
if (!user) return res.json({ found: false })
|
||
|
||
const cpf = cleanCpf(user.cpf)
|
||
|
||
// Tickets da rifa ativa
|
||
const tickets = await query(
|
||
`SELECT t.numero, r.titulo AS sorteio, t.data_compra
|
||
FROM tickets t
|
||
JOIN raffles r ON r.id = t.raffle_id
|
||
WHERE REGEXP_REPLACE(t.usuario_telefone, '\\D', '', 'g') = $1
|
||
AND r.ativo = 1
|
||
ORDER BY t.numero ASC`,
|
||
[clean]
|
||
)
|
||
|
||
// Clube(s) ativo(s)
|
||
const clubes = cpf ? await query(
|
||
`SELECT cm.id, cm.status, cm.next_billing_at,
|
||
cp.name AS plano, cp.price_monthly,
|
||
bc.name AS clube, bc.slug AS clube_slug, bc.specialty
|
||
FROM club_members cm
|
||
JOIN club_plans cp ON cp.id = cm.plan_id
|
||
JOIN benefit_clubs bc ON bc.id = cm.club_id
|
||
WHERE cm.user_cpf = $1 AND cm.status IN ('active','pending_payment')
|
||
ORDER BY cm.created_at DESC`,
|
||
[cpf]
|
||
) : []
|
||
|
||
// Candidaturas ativas (últimas 3)
|
||
const candidaturas = cpf ? await query(
|
||
`SELECT ja.status, ja.applied_at,
|
||
j.title AS vaga, j.work_mode, j.job_type, j.location
|
||
FROM job_applications ja
|
||
JOIN jobs j ON j.id = ja.job_id
|
||
WHERE ja.applicant_cpf = $1
|
||
ORDER BY ja.applied_at DESC
|
||
LIMIT 3`,
|
||
[cpf]
|
||
) : []
|
||
|
||
res.json({
|
||
found: true,
|
||
cliente: {
|
||
...user,
|
||
tickets,
|
||
clubes,
|
||
candidaturas,
|
||
},
|
||
})
|
||
} catch (err) {
|
||
console.error('[nw/cliente]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/clube?cpf= ────────────────────────────────────────────────────
|
||
// Retorna plano ativo do cliente, clube, dependentes e parceiros próximos
|
||
router.get('/clube', async (req, res) => {
|
||
const { cpf } = req.query
|
||
if (!cpf) return res.status(400).json({ error: '"cpf" é obrigatório' })
|
||
|
||
const cleanedCpf = cleanCpf(cpf)
|
||
|
||
try {
|
||
const memberships = await query(
|
||
`SELECT cm.id, cm.status, cm.next_billing_at, cm.started_at,
|
||
cp.name AS plano, cp.price_monthly, cp.max_dependents, cp.features,
|
||
bc.id AS clube_id, bc.name AS clube, bc.slug AS clube_slug,
|
||
bc.specialty, bc.conditions, bc.rules
|
||
FROM club_members cm
|
||
JOIN club_plans cp ON cp.id = cm.plan_id
|
||
JOIN benefit_clubs bc ON bc.id = cm.club_id
|
||
WHERE cm.user_cpf = $1
|
||
ORDER BY cm.status = 'active' DESC, cm.created_at DESC`,
|
||
[cleanedCpf]
|
||
)
|
||
|
||
if (!memberships.length) return res.json({ encontrado: false, memberships: [] })
|
||
|
||
// Para cada associação ativa, busca dependentes e parceiros
|
||
const result = await Promise.all(memberships.map(async (m) => {
|
||
const [dependentes, parceiros] = await Promise.all([
|
||
query(
|
||
`SELECT nome, parentesco, cpf, nascimento
|
||
FROM club_dependents
|
||
WHERE member_id = $1
|
||
ORDER BY nome ASC`,
|
||
[m.id]
|
||
),
|
||
query(
|
||
`SELECT name, specialty, phone, address, discount_percent, notes
|
||
FROM club_partners
|
||
WHERE club_id = $1 AND active = true
|
||
ORDER BY name ASC
|
||
LIMIT 10`,
|
||
[m.clube_id]
|
||
),
|
||
])
|
||
return { ...m, dependentes, parceiros }
|
||
}))
|
||
|
||
res.json({ encontrado: true, memberships: result })
|
||
} catch (err) {
|
||
console.error('[nw/clube]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/candidaturas?cpf= ─────────────────────────────────────────────
|
||
// Retorna todas as candidaturas do candidato com status detalhado
|
||
router.get('/candidaturas', async (req, res) => {
|
||
const { cpf } = req.query
|
||
if (!cpf) return res.status(400).json({ error: '"cpf" é obrigatório' })
|
||
|
||
const cleanedCpf = cleanCpf(cpf)
|
||
|
||
try {
|
||
const candidaturas = await query(
|
||
`SELECT ja.id, ja.status, ja.applied_at, ja.cover_letter,
|
||
j.title AS vaga, j.work_mode, j.job_type, j.location, j.category
|
||
FROM job_applications ja
|
||
JOIN jobs j ON j.id = ja.job_id
|
||
WHERE ja.applicant_cpf = $1
|
||
ORDER BY ja.applied_at DESC`,
|
||
[cleanedCpf]
|
||
)
|
||
|
||
const statusLabels = {
|
||
novo: 'Candidatura recebida',
|
||
em_analise: 'Em análise pela empresa',
|
||
aprovado: 'Aprovado! Aguarde contato',
|
||
reprovado: 'Não selecionado desta vez',
|
||
contratado: 'Contratado — parabéns!',
|
||
}
|
||
|
||
const enriched = candidaturas.map(c => ({
|
||
...c,
|
||
status_label: statusLabels[c.status] || c.status,
|
||
}))
|
||
|
||
res.json({ total: enriched.length, candidaturas: enriched })
|
||
} catch (err) {
|
||
console.error('[nw/candidaturas]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/parceiros?clube=slug ──────────────────────────────────────────
|
||
// Lista parceiros de um clube (para indicar onde o cliente pode usar o benefício)
|
||
router.get('/parceiros', async (req, res) => {
|
||
const { clube } = req.query
|
||
if (!clube) return res.status(400).json({ error: '"clube" (slug) é obrigatório' })
|
||
|
||
try {
|
||
const clubeData = await queryOne(
|
||
`SELECT id, name, specialty FROM benefit_clubs WHERE slug = $1 AND active = true`,
|
||
[clube]
|
||
)
|
||
if (!clubeData) return res.json({ encontrado: false, parceiros: [] })
|
||
|
||
const parceiros = await query(
|
||
`SELECT name, specialty, phone, address, discount_percent, notes
|
||
FROM club_partners
|
||
WHERE club_id = $1 AND active = true
|
||
ORDER BY name ASC`,
|
||
[clubeData.id]
|
||
)
|
||
|
||
res.json({
|
||
encontrado: true,
|
||
clube: { name: clubeData.name, specialty: clubeData.specialty },
|
||
parceiros,
|
||
})
|
||
} catch (err) {
|
||
console.error('[nw/parceiros]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/vagas ─────────────────────────────────────────────────────────
|
||
// Lista vagas abertas (para responder perguntas sobre oportunidades de emprego)
|
||
router.get('/vagas', async (_req, res) => {
|
||
try {
|
||
const vagas = await query(
|
||
`SELECT title, category, work_mode, job_type, location,
|
||
salary_min, salary_max, salary_hide, slots,
|
||
published_at, expires_at
|
||
FROM jobs
|
||
WHERE active = true
|
||
AND (expires_at IS NULL OR expires_at > NOW())
|
||
ORDER BY published_at DESC
|
||
LIMIT 20`
|
||
)
|
||
|
||
const workModeLabels = { presencial: 'Presencial', hibrido: 'Híbrido', remoto: 'Remoto' }
|
||
const jobTypeLabels = { clt: 'CLT', pj: 'PJ', freelance: 'Freelance', estagio: 'Estágio', temporario: 'Temporário' }
|
||
|
||
const enriched = vagas.map(v => ({
|
||
...v,
|
||
work_mode_label: workModeLabels[v.work_mode] || v.work_mode,
|
||
job_type_label: jobTypeLabels[v.job_type] || v.job_type,
|
||
salario: v.salary_hide || (!v.salary_min && !v.salary_max)
|
||
? 'A combinar'
|
||
: v.salary_min && v.salary_max
|
||
? `R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })} – R$ ${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`
|
||
: v.salary_min
|
||
? `A partir de R$ ${Number(v.salary_min).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`
|
||
: `Até R$ ${Number(v.salary_max).toLocaleString('pt-BR', { maximumFractionDigits: 0 })}`,
|
||
}))
|
||
|
||
res.json({ total: enriched.length, vagas: enriched })
|
||
} catch (err) {
|
||
console.error('[nw/vagas]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/promocoes ─────────────────────────────────────────────────────
|
||
router.get('/promocoes', async (_req, res) => {
|
||
try {
|
||
const promos = await query(
|
||
`SELECT id, nome, descricao, preco_original, preco_promocional
|
||
FROM promotions
|
||
WHERE ativo = 1
|
||
ORDER BY id ASC`
|
||
)
|
||
res.json(promos)
|
||
} catch (err) {
|
||
console.error('[nw/promocoes]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/sorteio ───────────────────────────────────────────────────────
|
||
router.get('/sorteio', async (_req, res) => {
|
||
try {
|
||
const sorteio = await queryOne(
|
||
"SELECT * FROM raffles WHERE ativo = 1 ORDER BY id DESC LIMIT 1"
|
||
)
|
||
if (!sorteio) return res.json({ ativo: false })
|
||
|
||
const countRow = await queryOne(
|
||
`SELECT COUNT(*) AS total
|
||
FROM tickets
|
||
WHERE raffle_id = $1
|
||
AND (status = 'sold' OR (status = 'reserved' AND reserved_until > NOW()))`,
|
||
[sorteio.id]
|
||
)
|
||
const total_vendidos = parseInt(countRow?.total ?? 0, 10)
|
||
|
||
res.json({
|
||
ativo: true,
|
||
sorteio: {
|
||
id: sorteio.id,
|
||
titulo: sorteio.titulo,
|
||
descricao: sorteio.descricao,
|
||
preco_numero: sorteio.preco_numero,
|
||
total_numeros: sorteio.total_numeros,
|
||
numeros_vendidos: total_vendidos,
|
||
numeros_disponiveis: sorteio.total_numeros - total_vendidos,
|
||
},
|
||
})
|
||
} catch (err) {
|
||
console.error('[nw/sorteio]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
// ── GET /nw/sorteio/tickets?telefone= ────────────────────────────────────
|
||
router.get('/sorteio/tickets', async (req, res) => {
|
||
const { telefone } = req.query
|
||
if (!telefone) return res.status(400).json({ error: '"telefone" é obrigatório' })
|
||
|
||
const clean = cleanPhone(telefone)
|
||
|
||
try {
|
||
const sorteio = await queryOne(
|
||
"SELECT id, titulo FROM raffles WHERE ativo = 1 ORDER BY id DESC LIMIT 1"
|
||
)
|
||
if (!sorteio) return res.json({ ativo: false, tickets: [] })
|
||
|
||
const tickets = await query(
|
||
`SELECT t.numero, t.data_compra
|
||
FROM tickets t
|
||
WHERE t.raffle_id = $1
|
||
AND REGEXP_REPLACE(t.usuario_telefone, '\\D', '', 'g') = $2
|
||
AND (t.status = 'sold' OR (t.status = 'reserved' AND t.reserved_until > NOW()))
|
||
ORDER BY t.numero ASC`,
|
||
[sorteio.id, clean]
|
||
)
|
||
|
||
res.json({
|
||
sorteio: sorteio.titulo,
|
||
telefone,
|
||
total: tickets.length,
|
||
tickets,
|
||
})
|
||
} catch (err) {
|
||
console.error('[nw/sorteio/tickets]', err.message)
|
||
res.status(500).json({ error: 'Erro interno' })
|
||
}
|
||
})
|
||
|
||
return router
|
||
}
|
||
|
||
module.exports = { createRoutes }
|