chore(ops): restore missing root files in newwhats.clube67.com
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)

This commit is contained in:
VPS 4 Deploy Agent
2026-05-18 03:27:25 +02:00
parent 5ec6bd6354
commit 0dc5eefa06
406 changed files with 237013 additions and 0 deletions
@@ -0,0 +1,243 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLeadsRoutes = createLeadsRoutes;
const express_1 = require("express");
let authenticate;
let authorize;
if (process.env.NODE_ENV === 'production') {
authenticate = require('../../../dist/middleware/auth').authMiddleware;
authorize = require('../../../dist/middleware/rbac').adminMiddleware;
}
else {
authenticate = require('../../../backend/src/middleware/auth').authMiddleware;
authorize = require('../../../backend/src/middleware/rbac').adminMiddleware;
}
const STATUS_VALUES = ['new', 'contacted', 'qualified', 'converted', 'lost'];
const LEAD_TYPE_VALUES = ['cold', 'member'];
function translateLead(lead) {
if (!lead)
return lead;
return {
...lead,
source: lead.source || 'manual',
partner_id: lead.partner_id || null,
benefit_id: lead.benefit_id || null,
user_id: lead.user_id || null,
lead_type: lead.lead_type || 'cold',
};
}
function createLeadsRoutes(ctx) {
const router = (0, express_1.Router)();
const { db } = ctx;
const protectedRoutes = [authenticate, authorize(['super_admin', 'admin'])];
// GET /api/leads/metrics — Global metrics for super admin
router.get('/metrics', ...protectedRoutes, async (req, res) => {
try {
const [totalRow] = await db('leads').count('id as total');
const total = Number(totalRow.total);
// Today
const [todayRow] = await db('leads')
.count('id as total')
.whereRaw('DATE(created_at) = CURDATE()');
const today = Number(todayRow.total);
// By status
const byStatus = await db('leads')
.select('status')
.count('id as total')
.groupBy('status');
// By lead type
const byType = await db('leads')
.select('lead_type')
.count('id as total')
.groupBy('lead_type');
// Converted count for conversion rate
const converted = byStatus.find((r) => r.status === 'converted');
const conversionRate = total > 0 ? ((Number(converted?.total || 0) / total) * 100).toFixed(1) : '0.0';
// By city (via users join)
const byCity = await db('leads')
.join('users', 'leads.user_id', 'users.id')
.select('users.city', 'users.state')
.count('leads.id as total')
.whereNotNull('users.city')
.groupBy('users.city', 'users.state')
.orderBy('total', 'desc')
.limit(10);
// By neighborhood (via users join)
const byNeighborhood = await db('leads')
.join('users', 'leads.user_id', 'users.id')
.select('users.neighborhood', 'users.city', 'users.state')
.count('leads.id as total')
.whereNotNull('users.neighborhood')
.groupBy('users.neighborhood', 'users.city', 'users.state')
.orderBy('total', 'desc')
.limit(10);
// By partner
const byPartner = await db('leads')
.join('partners', 'leads.partner_id', 'partners.id')
.select('partners.id as partner_id', 'partners.company_name')
.count('leads.id as total')
.groupBy('partners.id', 'partners.company_name')
.orderBy('total', 'desc')
.limit(10);
// Recent leads
const recent = await db('leads')
.leftJoin('users', 'leads.user_id', 'users.id')
.leftJoin('partners', 'leads.partner_id', 'partners.id')
.select('leads.id', 'leads.name', 'leads.email', 'leads.status', 'leads.lead_type', 'leads.source', 'leads.created_at', 'partners.company_name as partner_name', 'users.city', 'users.neighborhood')
.orderBy('leads.created_at', 'desc')
.limit(15);
res.json({
total,
today,
conversion_rate: conversionRate,
by_status: byStatus,
by_type: byType,
by_city: byCity,
by_neighborhood: byNeighborhood,
by_partner: byPartner,
recent,
});
}
catch (err) {
console.error('Error fetching lead metrics:', err);
res.status(500).json({ error: 'Erro ao buscar métricas' });
}
});
router.get('/', authenticate, async (req, res) => {
try {
const { partner_id, status, type, page = '1', limit = '20' } = req.query;
const offset = (parseInt(page) - 1) * parseInt(limit);
const userRole = req.user?.role;
const userPartner = req.user?.partner_id;
let query = db('leads').orderBy('created_at', 'desc');
if (partner_id)
query = query.where({ partner_id });
if (userRole === 'partner' && userPartner) {
query = query.where({ partner_id: userPartner });
}
if (type && LEAD_TYPE_VALUES.includes(type)) {
query = query.where({ lead_type: type });
}
if (status && STATUS_VALUES.includes(status)) {
query = query.where({ status: status });
}
else {
query = query.where({ status: 'new' });
}
const countResult = await query.clone().count('id as count');
const leads = await query.limit(parseInt(limit)).offset(offset);
res.json({ leads: leads.map(translateLead), total: Number(countResult[0].count), page: parseInt(page) });
}
catch (err) {
res.status(500).json({ error: 'Erro ao listar leads' });
}
});
router.get('/:id', authenticate, async (req, res) => {
try {
const id = Number(req.params.id);
const lead = await db('leads').where({ id }).first();
if (!lead)
return res.status(404).json({ error: 'Lead não encontrado' });
if (req.user?.role === 'partner' && req.user.partner_id && lead.partner_id !== req.user.partner_id) {
return res.status(403).json({ error: 'Acesso negado' });
}
const history = await db('lead_history').where({ lead_id: id }).orderBy('created_at', 'desc');
res.json({ lead: translateLead(lead), history });
}
catch (err) {
res.status(500).json({ error: 'Erro ao buscar lead' });
}
});
router.post('/', ...protectedRoutes, async (req, res) => {
try {
const { name, email, phone, partner_id, source, notes, benefit_id, assigned_to, user_id } = req.body;
const numericUserId = user_id ? Number(user_id) : null;
const parsedUserId = Number.isInteger(numericUserId) ? numericUserId : null;
const leadType = parsedUserId ? 'member' : 'cold';
const [id] = await db('leads').insert({
name,
email,
phone,
partner_id: partner_id || null,
benefit_id: benefit_id || null,
user_id: parsedUserId,
lead_type: leadType,
source: source || 'manual',
notes: notes || null,
assigned_to: assigned_to || null,
status: 'new',
owner: 'clube67',
});
await db('lead_history').insert({
lead_id: id,
action: 'created',
new_value: 'new',
created_by: req.user?.id || null,
});
await ctx.hooks.emit('lead:created', { id, email, partner_id });
const lead = await db('leads').where({ id }).first();
res.status(201).json(translateLead(lead));
}
catch (err) {
res.status(500).json({ error: 'Erro ao criar lead' });
}
});
router.patch('/:id/status', ...protectedRoutes, async (req, res) => {
try {
const id = Number(req.params.id);
const { status } = req.body;
if (!STATUS_VALUES.includes(status))
return res.status(400).json({ error: 'Status inválido' });
const lead = await db('leads').where({ id }).first();
if (!lead)
return res.status(404).json({ error: 'Lead não encontrado' });
await db('leads').where({ id }).update({ status, updated_at: db.fn.now() });
await db('lead_history').insert({
lead_id: id,
action: 'status',
old_value: lead.status,
new_value: status,
created_by: req.user?.id || null,
});
if (status === 'converted')
await ctx.hooks.emit('lead:converted', { id });
if (status === 'lost')
await ctx.hooks.emit('lead:lost', { id });
const updated = await db('leads').where({ id }).first();
res.json(translateLead(updated));
}
catch (err) {
res.status(500).json({ error: 'Erro ao atualizar status' });
}
});
router.post('/:id/notes', ...protectedRoutes, async (req, res) => {
try {
const id = Number(req.params.id);
const { note } = req.body;
if (!note)
return res.status(400).json({ error: 'Nota obrigatória' });
const lead = await db('leads').where({ id }).first();
if (!lead)
return res.status(404).json({ error: 'Lead não encontrado' });
await db('lead_history').insert({
lead_id: id,
action: 'note',
old_value: lead.notes,
new_value: lead.notes ? `${lead.notes}\n${note}` : note,
note,
created_by: req.user?.id || null,
});
await db('leads').where({ id }).update({
notes: db.raw('CONCAT(COALESCE(notes,""), "\n", ?)', [note]),
updated_at: db.fn.now(),
});
const updated = await db('leads').where({ id }).first();
res.json(translateLead(updated));
}
catch (err) {
res.status(500).json({ error: 'Erro ao salvar nota' });
}
});
return router;
}
//# sourceMappingURL=routes.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,256 @@
import { Router, Request, Response } from 'express';
import { PluginContext } from '../../../backend/src/core/types';
let authenticate: any;
let authorize: any;
if (process.env.NODE_ENV === 'production') {
authenticate = require('../../../dist/middleware/auth').authMiddleware;
authorize = require('../../../dist/middleware/rbac').adminMiddleware;
} else {
authenticate = require('../../../backend/src/middleware/auth').authMiddleware;
authorize = require('../../../backend/src/middleware/rbac').adminMiddleware;
}
const STATUS_VALUES = ['new', 'contacted', 'qualified', 'converted', 'lost'];
const LEAD_TYPE_VALUES = ['cold', 'member'];
function translateLead(lead: any) {
if (!lead) return lead;
return {
...lead,
source: lead.source || 'manual',
partner_id: lead.partner_id || null,
benefit_id: lead.benefit_id || null,
user_id: lead.user_id || null,
lead_type: lead.lead_type || 'cold',
};
}
export function createLeadsRoutes(ctx: PluginContext): Router {
const router = Router();
const { db } = ctx;
const protectedRoutes = [authenticate, authorize(['super_admin', 'admin'])];
// GET /api/leads/metrics — Global metrics for super admin
router.get('/metrics', ...protectedRoutes, async (req: Request, res: Response) => {
try {
const [totalRow] = await db('leads').count('id as total');
const total = Number(totalRow.total);
// Today
const [todayRow] = await db('leads')
.count('id as total')
.whereRaw('DATE(created_at) = CURDATE()');
const today = Number(todayRow.total);
// By status
const byStatus = await db('leads')
.select('status')
.count('id as total')
.groupBy('status');
// By lead type
const byType = await db('leads')
.select('lead_type')
.count('id as total')
.groupBy('lead_type');
// Converted count for conversion rate
const converted = byStatus.find((r: any) => r.status === 'converted');
const conversionRate = total > 0 ? ((Number(converted?.total || 0) / total) * 100).toFixed(1) : '0.0';
// By city (via users join)
const byCity = await db('leads')
.join('users', 'leads.user_id', 'users.id')
.select('users.city', 'users.state')
.count('leads.id as total')
.whereNotNull('users.city')
.groupBy('users.city', 'users.state')
.orderBy('total', 'desc')
.limit(10);
// By neighborhood (via users join)
const byNeighborhood = await db('leads')
.join('users', 'leads.user_id', 'users.id')
.select('users.neighborhood', 'users.city', 'users.state')
.count('leads.id as total')
.whereNotNull('users.neighborhood')
.groupBy('users.neighborhood', 'users.city', 'users.state')
.orderBy('total', 'desc')
.limit(10);
// By partner
const byPartner = await db('leads')
.join('partners', 'leads.partner_id', 'partners.id')
.select('partners.id as partner_id', 'partners.company_name')
.count('leads.id as total')
.groupBy('partners.id', 'partners.company_name')
.orderBy('total', 'desc')
.limit(10);
// Recent leads
const recent = await db('leads')
.leftJoin('users', 'leads.user_id', 'users.id')
.leftJoin('partners', 'leads.partner_id', 'partners.id')
.select(
'leads.id',
'leads.name',
'leads.email',
'leads.status',
'leads.lead_type',
'leads.source',
'leads.created_at',
'partners.company_name as partner_name',
'users.city',
'users.neighborhood',
)
.orderBy('leads.created_at', 'desc')
.limit(15);
res.json({
total,
today,
conversion_rate: conversionRate,
by_status: byStatus,
by_type: byType,
by_city: byCity,
by_neighborhood: byNeighborhood,
by_partner: byPartner,
recent,
});
} catch (err: any) {
console.error('Error fetching lead metrics:', err);
res.status(500).json({ error: 'Erro ao buscar métricas' });
}
});
router.get('/', authenticate, async (req: Request, res: Response) => {
try {
const { partner_id, status, type, page = '1', limit = '20' } = req.query;
const offset = (parseInt(page as string) - 1) * parseInt(limit as string);
const userRole = req.user?.role;
const userPartner = req.user?.partner_id;
let query = db('leads').orderBy('created_at', 'desc');
if (partner_id) query = query.where({ partner_id });
if (userRole === 'partner' && userPartner) {
query = query.where({ partner_id: userPartner });
}
if (type && LEAD_TYPE_VALUES.includes(type as string)) {
query = query.where({ lead_type: type as string });
}
if (status && STATUS_VALUES.includes(status as string)) {
query = query.where({ status: status as string });
} else {
query = query.where({ status: 'new' });
}
const countResult = await query.clone().count('id as count');
const leads = await query.limit(parseInt(limit as string)).offset(offset);
res.json({ leads: leads.map(translateLead), total: Number(countResult[0].count), page: parseInt(page as string) });
} catch (err: any) {
res.status(500).json({ error: 'Erro ao listar leads' });
}
});
router.get('/:id', authenticate, async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const lead = await db('leads').where({ id }).first();
if (!lead) return res.status(404).json({ error: 'Lead não encontrado' });
if (req.user?.role === 'partner' && req.user.partner_id && lead.partner_id !== req.user.partner_id) {
return res.status(403).json({ error: 'Acesso negado' });
}
const history = await db('lead_history').where({ lead_id: id }).orderBy('created_at', 'desc');
res.json({ lead: translateLead(lead), history });
} catch (err: any) {
res.status(500).json({ error: 'Erro ao buscar lead' });
}
});
router.post('/', ...protectedRoutes, async (req: Request, res: Response) => {
try {
const { name, email, phone, partner_id, source, notes, benefit_id, assigned_to, user_id } = req.body;
const numericUserId = user_id ? Number(user_id) : null;
const parsedUserId = Number.isInteger(numericUserId) ? numericUserId : null;
const leadType = parsedUserId ? 'member' : 'cold';
const [id] = await db('leads').insert({
name,
email,
phone,
partner_id: partner_id || null,
benefit_id: benefit_id || null,
user_id: parsedUserId,
lead_type: leadType,
source: source || 'manual',
notes: notes || null,
assigned_to: assigned_to || null,
status: 'new',
owner: 'clube67',
});
await db('lead_history').insert({
lead_id: id,
action: 'created',
new_value: 'new',
created_by: (req as any).user?.id || null,
});
await ctx.hooks.emit('lead:created', { id, email, partner_id });
const lead = await db('leads').where({ id }).first();
res.status(201).json(translateLead(lead));
} catch (err: any) {
res.status(500).json({ error: 'Erro ao criar lead' });
}
});
router.patch('/:id/status', ...protectedRoutes, async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const { status } = req.body;
if (!STATUS_VALUES.includes(status)) return res.status(400).json({ error: 'Status inválido' });
const lead = await db('leads').where({ id }).first();
if (!lead) return res.status(404).json({ error: 'Lead não encontrado' });
await db('leads').where({ id }).update({ status, updated_at: db.fn.now() });
await db('lead_history').insert({
lead_id: id,
action: 'status',
old_value: lead.status,
new_value: status,
created_by: (req as any).user?.id || null,
});
if (status === 'converted') await ctx.hooks.emit('lead:converted', { id });
if (status === 'lost') await ctx.hooks.emit('lead:lost', { id });
const updated = await db('leads').where({ id }).first();
res.json(translateLead(updated));
} catch (err: any) {
res.status(500).json({ error: 'Erro ao atualizar status' });
}
});
router.post('/:id/notes', ...protectedRoutes, async (req: Request, res: Response) => {
try {
const id = Number(req.params.id);
const { note } = req.body;
if (!note) return res.status(400).json({ error: 'Nota obrigatória' });
const lead = await db('leads').where({ id }).first();
if (!lead) return res.status(404).json({ error: 'Lead não encontrado' });
await db('lead_history').insert({
lead_id: id,
action: 'note',
old_value: lead.notes,
new_value: lead.notes ? `${lead.notes}\n${note}` : note,
note,
created_by: (req as any).user?.id || null,
});
await db('leads').where({ id }).update({
notes: db.raw('CONCAT(COALESCE(notes,""), "\n", ?)', [note]),
updated_at: db.fn.now(),
});
const updated = await db('leads').where({ id }).first();
res.json(translateLead(updated));
} catch (err: any) {
res.status(500).json({ error: 'Erro ao salvar nota' });
}
});
return router;
}