Plataforma completa de helpdesk com autenticação, abertura e gerenciamento de chamados, notificações por e-mail e painel administrativo.
Login seguro com hash de senha, níveis de acesso (admin/user) e sessão persistente com Flask-Login.
Criação de tickets com título, descrição detalhada, categorias (Hardware, Software, Rede, etc.) e prioridades.
Status workflow (aberto → em andamento → resolvido → fechado), atribuição a técnicos e notas internas para admins.
Notificações por e-mail (Outlook) para novo chamado, respostas e mudanças de status com template HTML profissional.
Visão geral com contadores (total, abertos, em andamento, resolvidos, urgentes) e chamados recentes.
Admin pode criar, ativar/desativar usuários e resetar senhas. Usuários editam próprio perfil e alteram senha.
"""
Sistema de Chamado TI — HelpDesk
Flask + SQLAlchemy + Flask-Login + Flask-Mail
"""
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_mail import Mail, Message
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
import threading
app = Flask(__name__)
app.config['SECRET_KEY'] = '***'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///chamados.db'
# E-mail (Outlook)
app.config['MAIL_SERVER'] = 'email-ssl.com.br'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = 'helpdesk@empresa.com'
app.config['MAIL_PASSWORD'] = '***'
MAIL_ATIVO = bool(app.config['MAIL_USERNAME'])
db = SQLAlchemy(app)
mail = Mail(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
# ─── Models: Usuario, Chamado, Resposta ───
# (ver aba Models)
# ─── Rotas de Autenticação ───
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username', '').strip()
senha = request.form.get('senha', '')
user = Usuario.query.filter_by(username=username).first()
if user and user.ativo and user.check_senha(senha):
login_user(user, remember=True)
return redirect(url_for('dashboard'))
flash('Usuario ou senha incorretos.', 'erro')
return render_template('login.html')
# ─── Dashboard ───
@app.route('/dashboard')
@login_required
def dashboard():
if current_user.is_admin:
total = Chamado.query.count()
abertos = Chamado.query.filter_by(status='aberto').count()
urgentes = Chamado.query.filter_by(prioridade='urgente', status='aberto').count()
recentes = Chamado.query.order_by(Chamado.criado_em.desc()).limit(10).all()
return render_template('dashboard_admin.html', ...)
meus = Chamado.query.filter_by(usuario_id=current_user.id).order_by(Chamado.criado_em.desc()).all()
return render_template('dashboard_user.html', chamados=meus)
# ─── Chamados CRUD ───
@app.route('/chamado/novo', methods=['GET', 'POST'])
@login_required
def novo_chamado():
if request.method == 'POST':
c = Chamado(titulo=request.form['titulo'], descricao=request.form['descricao'],
categoria=request.form['categoria'], prioridade=request.form['prioridade'],
usuario_id=current_user.id)
db.session.add(c)
db.session.commit()
notificar_novo_chamado(c)
flash(f'Chamado #{c.id} aberto com sucesso!', 'sucesso')
return redirect(url_for('ver_chamado', id=c.id))
return render_template('novo_chamado.html')
@app.route('/chamado/<int:id>/responder', methods=['POST'])
@login_required
def responder_chamado(id):
c = Chamado.query.get_or_404(id)
msg = request.form.get('mensagem', '').strip()
is_interno = request.form.get('is_interno') == '1' and current_user.is_admin
r = Resposta(chamado_id=id, usuario_id=current_user.id, mensagem=msg, is_interno=is_interno)
db.session.add(r)
db.session.commit()
notificar_resposta_chamado(c, r)
return redirect(url_for('ver_chamado', id=id))
@app.route('/chamado/<int:id>/atualizar', methods=['POST'])
@login_required
def atualizar_chamado(id):
c = Chamado.query.get_or_404(id)
status_anterior = c.status
c.status = request.form.get('status')
c.prioridade = request.form.get('prioridade')
c.atribuido_a = int(request.form.get('atribuido_a')) if request.form.get('atribuido_a') else None
if c.status == 'resolvido':
c.resolvido_em = datetime.utcnow()
db.session.commit()
notificar_status_chamado(c, status_anterior)
return redirect(url_for('ver_chamado', id=id))
# ─── Notificações E-mail ───
def notificar_novo_chamado(chamado):
admins = Usuario.query.filter_by(is_admin=True, ativo=True).all()
destinos = [a.email for a in admins if a.email]
corpo = _html_email(
f'Novo Chamado #{chamado.id} aberto',
'Um usuario abriu um chamado e aguarda atendimento.',
[('Chamado', f'#{chamado.id}'), ('Titulo', chamado.titulo), ...]
)
enviar_email(destinos, f'[HelpDesk] Novo Chamado #{chamado.id}', corpo)
# ─── Main ───
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=False, host='192.168.1.219', port=50000)
# ─── Modelos do Banco de Dados ───
class Usuario(UserMixin, db.Model):
__tablename__ = 'usuarios'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
senha_hash = db.Column(db.String(256), nullable=False)
nome_completo = db.Column(db.String(150), nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
setor = db.Column(db.String(100))
is_admin = db.Column(db.Boolean, default=False)
ativo = db.Column(db.Boolean, default=True)
criado_em = db.Column(db.DateTime, default=datetime.utcnow)
def set_senha(self, senha):
self.senha_hash = generate_password_hash(senha)
def check_senha(self, senha):
return check_password_hash(self.senha_hash, senha)
class Chamado(db.Model):
__tablename__ = 'chamados'
id = db.Column(db.Integer, primary_key=True)
titulo = db.Column(db.String(200), nullable=False)
descricao = db.Column(db.Text, nullable=False)
categoria = db.Column(db.String(100), nullable=False)
prioridade = db.Column(db.String(20), default='media')
status = db.Column(db.String(30), default='aberto')
usuario_id = db.Column(db.Integer, db.ForeignKey('usuarios.id'), nullable=False)
atribuido_a = db.Column(db.Integer, db.ForeignKey('usuarios.id'))
criado_em = db.Column(db.DateTime, default=datetime.utcnow)
atualizado_em = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
resolvido_em = db.Column(db.DateTime)
respostas = db.relationship('Resposta', backref='chamado',
lazy=True, cascade='all, delete-orphan')
# Relacionamentos
autor = db.relationship('Usuario', foreign_keys=[usuario_id], backref='chamados_abertos')
responsavel = db.relationship('Usuario', foreign_keys=[atribuido_a], backref='chamados_atribuidos')
class Resposta(db.Model):
__tablename__ = 'respostas'
id = db.Column(db.Integer, primary_key=True)
chamado_id = db.Column(db.Integer, db.ForeignKey('chamados.id'), nullable=False)
usuario_id = db.Column(db.Integer, db.ForeignKey('usuarios.id'), nullable=False)
mensagem = db.Column(db.Text, nullable=False)
is_interno = db.Column(db.Boolean, default=False)
criado_em = db.Column(db.DateTime, default=datetime.utcnow)
usuario = db.relationship('Usuario', backref='respostas')
<!-- Jinja2 Templates (Flask) -->
┌─ base.html ─────────────────────────────────────────────┐
│ Layout base com navbar, flash messages e bloco content │
└─────────────────────────────────────────────────────────┘
<!-- Navbar com navegação condicional por role -->
<nav class="navbar">
<span class="brand-icon">⚡</span>
<span class="brand-text">HelpDesk</span>
<a href="{{ url_for('dashboard') }}">Dashboard</a>
<a href="{{ url_for('listar_chamados') }}">Chamados</a>
{% if not current_user.is_admin %}
<a href="{{ url_for('novo_chamado') }}">+ Novo Chamado</a>
{% endif %}
{% if current_user.is_admin %}
<a href="{{ url_for('admin_usuarios') }}">Usuários</a>
{% endif %}
<a href="{{ url_for('logout') }}">Sair</a>
</nav>
┌─ login.html ───────────────────────────────────────────┐
│ Tela de login com design moderno (gradiente + blob) │
│ Formulário: usuário + senha + botão "Entrar" │
└─────────────────────────────────────────────────────────┘
<div class="login-wrapper">
<div class="login-art">
<h1>HelpDesk</h1>
<p>Sistema de Chamados</p>
</div>
<div class="login-form-area">
<form method="POST">
<input type="text" name="username" placeholder="Usuario">
<input type="password" name="senha" placeholder="Senha">
<button type="submit">Entrar</button>
</form>
</div>
</div>
┌─ dashboard_admin.html ──────────────────────────────────┐
│ Stats: total, abertos, em_andamento, resolvidos │
│ Tabela: chamados recentes (últimos 10) │
│ Botão: + Novo Usuário │
└─────────────────────────────────────────────────────────┘
{% block content %}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">{{ total }}</div>
<div class="stat-label">Total de Chamados</div>
</div>
<!-- ... abertos, em_andamento, resolvidos, urgentes ... -->
</div>
{% endblock %}
┌─ ver_chamado.html ─────────────────────────────────────┐
│ Timeline: histórico do chamado + respostas │
│ Sidebar (admin): alterar status, prioridade, atribuição │
│ Formulário de resposta (com opção de nota interna) │
└─────────────────────────────────────────────────────────┘
<div class="chamado-layout">
<div class="chamado-main">
<!-- Card do chamado -->
<!-- Timeline de respostas -->
<!-- Formulário de resposta -->
</div>
<div class="chamado-sidebar">
<!-- Gerenciar: status, prioridade, atribuição -->
<!-- Informações do chamado -->
</div>
</div>