Sistema de Chamado TI — HelpDesk

Plataforma completa de helpdesk com autenticação, abertura e gerenciamento de chamados, notificações por e-mail e painel administrativo.

PythonFlaskSQLiteFlask-LoginFlask-MailJinja2

🔐 Autenticação

Login seguro com hash de senha, níveis de acesso (admin/user) e sessão persistente com Flask-Login.

📋 Abertura de Chamados

Criação de tickets com título, descrição detalhada, categorias (Hardware, Software, Rede, etc.) e prioridades.

⚙️ Gestão de Tickets

Status workflow (aberto → em andamento → resolvido → fechado), atribuição a técnicos e notas internas para admins.

📧 E-mail Automático

Notificações por e-mail (Outlook) para novo chamado, respostas e mudanças de status com template HTML profissional.

📊 Dashboard Admin

Visão geral com contadores (total, abertos, em andamento, resolvidos, urgentes) e chamados recentes.

👥 Gestão de Usuários

Admin pode criar, ativar/desativar usuários e resetar senhas. Usuários editam próprio perfil e alteram senha.

Pré-visualização

HelpDesk - Sistema de Chamados
Total
47
Abertos
12
Em Andamento
8
Resolvidos
27
#TítuloPrioridadeStatus
#42PC não ligaUrgenteAberto
#41Sem acesso e-mailAltaEm andamento
#40Atualizar OfficeBaixaResolvido

Código do Projeto

app.py
Models
Templates
"""
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>