import io import csv from flask import Blueprint, request, jsonify, Response from flask_jwt_extended import jwt_required, get_jwt_identity from app.extensions import db from app.models.lead import Lead from app.models.lead_interacao import LeadInteracao from app.services.geocoding_service import GeocodingService from app.services.scraper_service import ScraperService from app.utils.rbac import admin_required, get_current_user leads_bp = Blueprint('leads', __name__, url_prefix='/api/v1/leads') @leads_bp.route('/scrape-preview', methods=['POST']) @jwt_required() def scrape_preview(): """ Realiza a varredura no Google Maps e retorna os estabelecimentos encontrados para visualização prévia e seleção ANTES de salvar no banco de dados. """ data = request.get_json() or {} cep = data.get('cep') ramo = data.get('ramo') max_results = int(data.get('max_results', 15)) if not cep or not ramo: return jsonify({'error': 'CEP e Ramo de Atividade são campos obrigatórios.'}), 400 location_info = GeocodingService.get_location_by_cep(cep) if 'error' in location_info and not location_info.get('cidade'): return jsonify({'error': location_info['error']}), 400 search_query = GeocodingService.build_search_query(ramo, location_info) scraped_data = ScraperService.scrape_google_maps(search_query, max_results=max_results) return jsonify({ 'search_query': search_query, 'location': location_info, 'count': len(scraped_data), 'results': scraped_data }), 200 @leads_bp.route('/import-selected', methods=['POST']) @jwt_required() def import_selected(): """ Persiste no banco de dados APENAS os leads selecionados pelo usuário na tela. """ data = request.get_json() or {} cep = data.get('cep') ramo = data.get('ramo') selected_leads = data.get('selected_leads', []) if not cep or not ramo: return jsonify({'error': 'CEP e Ramo de Atividade são obrigatórios.'}), 400 if not selected_leads or not isinstance(selected_leads, list): return jsonify({'error': 'Nenhum lead selecionado para importação.'}), 400 user_id = get_jwt_identity() summary = ScraperService.process_and_persist_leads(cep, ramo, selected_leads, usuario_id=user_id) return jsonify({ 'message': f'{summary["created_count"]} novos leads importados e {summary["updated_count"]} atualizados com sucesso!', 'summary': summary }), 200 @leads_bp.route('/search-maps', methods=['POST']) @jwt_required() def search_maps(): """ Endpoint legados de busca direta com salvamento automático. """ data = request.get_json() or {} cep = data.get('cep') ramo = data.get('ramo') max_results = int(data.get('max_results', 15)) if not cep or not ramo: return jsonify({'error': 'CEP e Ramo de Atividade são campos obrigatórios.'}), 400 location_info = GeocodingService.get_location_by_cep(cep) if 'error' in location_info and not location_info.get('cidade'): return jsonify({'error': location_info['error']}), 400 search_query = GeocodingService.build_search_query(ramo, location_info) scraped_data = ScraperService.scrape_google_maps(search_query, max_results=max_results) user_id = get_jwt_identity() summary = ScraperService.process_and_persist_leads(cep, ramo, scraped_data, usuario_id=user_id) return jsonify({ 'search_query': search_query, 'summary': summary }), 200 @leads_bp.route('', methods=['GET']) @jwt_required() def list_leads(): status_funil = request.args.get('status_funil') cidade = request.args.get('cidade') ramo = request.args.get('ramo') search = request.args.get('search') page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 200)) query = Lead.query if status_funil: query = query.filter(Lead.status_funil == status_funil) if cidade: query = query.filter(Lead.cidade.ilike(f"%{cidade}%")) if ramo: query = query.filter(Lead.ramo_atividade.ilike(f"%{ramo}%")) if search: pattern = f"%{search}%" query = query.filter( db.or_( Lead.nome_empresa.ilike(pattern), Lead.telefone.ilike(pattern), Lead.notas.ilike(pattern), Lead.bairro.ilike(pattern) ) ) pagination = query.order_by(Lead.atualizado_em.desc()).paginate(page=page, per_page=per_page, error_out=False) return jsonify({ 'leads': [l.to_dict() for l in pagination.items], 'total': pagination.total, 'pages': pagination.pages, 'page': pagination.page }), 200 @leads_bp.route('/', methods=['GET']) @jwt_required() def get_lead(lead_id): lead = db.session.get(Lead, lead_id) if not lead: return jsonify({'error': 'Lead não encontrado.'}), 404 interacoes = LeadInteracao.query.filter_by(lead_id=lead.id).order_by(LeadInteracao.timestamp.desc()).all() lead_data = lead.to_dict() lead_data['interacoes'] = [i.to_dict() for i in interacoes] return jsonify(lead_data), 200 @leads_bp.route('/', methods=['PUT']) @jwt_required() def update_lead(lead_id): lead = db.session.get(Lead, lead_id) if not lead: return jsonify({'error': 'Lead não encontrado.'}), 404 data = request.get_json() or {} user_id = get_jwt_identity() old_status = lead.status_funil new_status = data.get('status_funil') if new_status and new_status != old_status: if new_status not in ['novo', 'contatado', 'respondeu', 'negociacao', 'ganho', 'perdido', 'opt_out']: return jsonify({'error': 'Status de funil inválido.'}), 400 lead.status_funil = new_status interacao = LeadInteracao( lead_id=lead.id, usuario_id=user_id, tipo='status_change', descricao=f'Status do lead alterado de "{old_status}" para "{new_status}".', metadados={'old_status': old_status, 'new_status': new_status} ) db.session.add(interacao) if 'notas' in data and data['notas'] != lead.notas: lead.notas = data['notas'] interacao_nota = LeadInteracao( lead_id=lead.id, usuario_id=user_id, tipo='nota_adicionada', descricao='Notas do lead atualizadas.', metadados={} ) db.session.add(interacao_nota) if 'tags' in data and isinstance(data['tags'], list): lead.tags = data['tags'] if 'usuario_responsavel_id' in data: lead.usuario_responsavel_id = data['usuario_responsavel_id'] if 'telefone' in data: lead.telefone = data['telefone'] if 'website' in data: lead.website = data['website'] lead.atualizado_em = db.func.now() db.session.commit() return jsonify(lead.to_dict()), 200 @leads_bp.route('//opt-out', methods=['POST']) @jwt_required() def opt_out_lead(lead_id): lead = db.session.get(Lead, lead_id) if not lead: return jsonify({'error': 'Lead não encontrado.'}), 404 user_id = get_jwt_identity() lead.status_funil = 'opt_out' lead.atualizado_em = db.func.now() interacao = LeadInteracao( lead_id=lead.id, usuario_id=user_id, tipo='opt_out', descricao='Solicitação de exclusão / Opt-Out de contato (Conformidade LGPD).', metadados={'lgpd_opt_out': True} ) db.session.add(interacao) db.session.commit() return jsonify({'message': f'Lead {lead.nome_empresa} marcado como Opt-Out (LGPD).', 'lead': lead.to_dict()}), 200 @leads_bp.route('/export', methods=['GET']) @jwt_required() def export_leads(): format_type = request.args.get('format', 'csv').lower() status_funil = request.args.get('status_funil') query = Lead.query if status_funil: query = query.filter(Lead.status_funil == status_funil) leads = query.order_by(Lead.criado_em.desc()).all() if format_type == 'json': return jsonify([l.to_dict() for l in leads]), 200 output = io.StringIO() writer = csv.writer(output, delimiter=';') writer.writerow([ 'ID', 'Nome Empresa', 'Ramo Atividade', 'CEP', 'Logradouro', 'Bairro', 'Cidade', 'UF', 'Telefone', 'Telefone E.164', 'Google Rating', 'Total Avaliações', 'Status Funil', 'Website', 'Notas', 'Criado Em' ]) for l in leads: writer.writerow([ l.id, l.nome_empresa, l.ramo_atividade, l.cep_busca, l.logradouro, l.bairro, l.cidade, l.uf, l.telefone, l.telefone_sanitizado, l.google_rating, l.total_avaliacoes, l.status_funil, l.website, l.notas, l.criado_em.isoformat() if l.criado_em else '' ]) csv_data = output.getvalue() return Response( csv_data, mimetype='text/csv', headers={'Content-Disposition': 'attachment; filename=leads_export_leadradar.csv'} ) @leads_bp.route('/', methods=['DELETE']) @jwt_required() def delete_lead(lead_id): lead = db.session.get(Lead, lead_id) if not lead: return jsonify({'error': 'Lead não encontrado.'}), 404 LeadInteracao.query.filter_by(lead_id=lead.id).delete() db.session.delete(lead) db.session.commit() return jsonify({'message': f'Lead {lead.nome_empresa} excluído com sucesso.'}), 200 @leads_bp.route('/bulk', methods=['DELETE']) @admin_required() def expunge_leads(): LeadInteracao.query.delete() num_deleted = Lead.query.delete() db.session.commit() return jsonify({'message': f'Expurgo concluído. {num_deleted} leads removidos do banco.'}), 200