Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- .gitattributes +1 -0
- app.py +182 -0
- config.py +16 -0
- instance/pmbok.db +0 -0
- models.py +215 -0
- requirements.txt +5 -0
- static/uploads/1_mermaid-diagram-2025-06-25-183414.png +3 -0
- templates/base.html +49 -0
- templates/create_project.html +40 -0
- templates/index.html +54 -0
- templates/login.html +35 -0
- templates/project.html +142 -0
- templates/register.html +35 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/uploads/1_mermaid-diagram-2025-06-25-183414.png filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import (
|
| 2 |
+
Flask,
|
| 3 |
+
render_template,
|
| 4 |
+
redirect,
|
| 5 |
+
url_for,
|
| 6 |
+
flash,
|
| 7 |
+
request,
|
| 8 |
+
send_from_directory,
|
| 9 |
+
)
|
| 10 |
+
from werkzeug.utils import secure_filename
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
import os
|
| 13 |
+
from models import db, User, Project, Task, PMBOK_PROCESSES
|
| 14 |
+
from config import Config
|
| 15 |
+
from flask_login import (
|
| 16 |
+
LoginManager,
|
| 17 |
+
login_user,
|
| 18 |
+
logout_user,
|
| 19 |
+
login_required,
|
| 20 |
+
current_user,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
app = Flask(__name__)
|
| 24 |
+
app.config.from_object(Config)
|
| 25 |
+
|
| 26 |
+
# Inicializaci贸n de extensiones
|
| 27 |
+
db.init_app(app)
|
| 28 |
+
login_manager = LoginManager()
|
| 29 |
+
login_manager.init_app(app)
|
| 30 |
+
login_manager.login_view = "login"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@login_manager.user_loader
|
| 34 |
+
def load_user(id):
|
| 35 |
+
return User.query.get(int(id))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def allowed_file(filename):
|
| 39 |
+
ALLOWED_EXTENSIONS = {"pdf", "docx", "jpg", "jpeg", "png"}
|
| 40 |
+
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@app.route("/")
|
| 44 |
+
@login_required
|
| 45 |
+
def index():
|
| 46 |
+
projects = Project.query.filter_by(user_id=current_user.id).all()
|
| 47 |
+
return render_template("index.html", projects=projects)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@app.route("/project/new", methods=["GET", "POST"])
|
| 51 |
+
@login_required
|
| 52 |
+
def create_project():
|
| 53 |
+
if request.method == "POST":
|
| 54 |
+
name = request.form.get("name")
|
| 55 |
+
description = request.form.get("description")
|
| 56 |
+
start_date = datetime.strptime(request.form.get("start_date"), "%Y-%m-%d")
|
| 57 |
+
end_date = (
|
| 58 |
+
datetime.strptime(request.form.get("end_date"), "%Y-%m-%d")
|
| 59 |
+
if request.form.get("end_date")
|
| 60 |
+
else None
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
project = Project(
|
| 64 |
+
name=name,
|
| 65 |
+
description=description,
|
| 66 |
+
start_date=start_date,
|
| 67 |
+
end_date=end_date,
|
| 68 |
+
user_id=current_user.id,
|
| 69 |
+
)
|
| 70 |
+
db.session.add(project)
|
| 71 |
+
|
| 72 |
+
# Crear las 49 tareas PMBOK para el proyecto
|
| 73 |
+
for process in PMBOK_PROCESSES:
|
| 74 |
+
task = Task(
|
| 75 |
+
process_name=process["name"],
|
| 76 |
+
process_group=process["group"],
|
| 77 |
+
knowledge_area=process["area"],
|
| 78 |
+
project=project,
|
| 79 |
+
)
|
| 80 |
+
db.session.add(task)
|
| 81 |
+
|
| 82 |
+
db.session.commit()
|
| 83 |
+
flash("Proyecto creado exitosamente", "success")
|
| 84 |
+
return redirect(url_for("index"))
|
| 85 |
+
|
| 86 |
+
return render_template("create_project.html")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@app.route("/project/<int:project_id>")
|
| 90 |
+
@login_required
|
| 91 |
+
def project_detail(project_id):
|
| 92 |
+
project = Project.query.get_or_404(project_id)
|
| 93 |
+
if project.user_id != current_user.id:
|
| 94 |
+
flash("No tienes permiso para ver este proyecto", "danger")
|
| 95 |
+
return redirect(url_for("index"))
|
| 96 |
+
return render_template("project.html", project=project)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
@app.route("/task/update/<int:task_id>", methods=["POST"])
|
| 100 |
+
@login_required
|
| 101 |
+
def update_task(task_id):
|
| 102 |
+
task = Task.query.get_or_404(task_id)
|
| 103 |
+
if task.project.user_id != current_user.id:
|
| 104 |
+
flash("No tienes permiso para modificar esta tarea", "danger")
|
| 105 |
+
return redirect(url_for("index"))
|
| 106 |
+
|
| 107 |
+
status = request.form.get("status")
|
| 108 |
+
file = request.files.get("evidence")
|
| 109 |
+
|
| 110 |
+
if status == "Terminado" and not (file or task.evidence_file):
|
| 111 |
+
flash(
|
| 112 |
+
"Debes subir una evidencia para marcar la tarea como terminada", "warning"
|
| 113 |
+
)
|
| 114 |
+
return redirect(url_for("project_detail", project_id=task.project_id))
|
| 115 |
+
|
| 116 |
+
if file and allowed_file(file.filename):
|
| 117 |
+
filename = secure_filename(f"{task.id}_{file.filename}")
|
| 118 |
+
file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
|
| 119 |
+
task.evidence_file = filename
|
| 120 |
+
|
| 121 |
+
task.status = status
|
| 122 |
+
if status == "Terminado":
|
| 123 |
+
task.completion_date = datetime.utcnow()
|
| 124 |
+
|
| 125 |
+
db.session.commit()
|
| 126 |
+
flash("Tarea actualizada exitosamente", "success")
|
| 127 |
+
return redirect(url_for("project_detail", project_id=task.project_id))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
@app.route("/uploads/<filename>")
|
| 131 |
+
@login_required
|
| 132 |
+
def download_file(filename):
|
| 133 |
+
return send_from_directory(app.config["UPLOAD_FOLDER"], filename)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
@app.route("/login", methods=["GET", "POST"])
|
| 137 |
+
def login():
|
| 138 |
+
if current_user.is_authenticated:
|
| 139 |
+
return redirect(url_for("index"))
|
| 140 |
+
|
| 141 |
+
if request.method == "POST":
|
| 142 |
+
user = User.query.filter_by(username=request.form["username"]).first()
|
| 143 |
+
if user and user.check_password(request.form["password"]):
|
| 144 |
+
login_user(user)
|
| 145 |
+
return redirect(url_for("index"))
|
| 146 |
+
flash("Usuario o contrase帽a incorrectos", "danger")
|
| 147 |
+
|
| 148 |
+
return render_template("login.html")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@app.route("/register", methods=["GET", "POST"])
|
| 152 |
+
def register():
|
| 153 |
+
if current_user.is_authenticated:
|
| 154 |
+
return redirect(url_for("index"))
|
| 155 |
+
|
| 156 |
+
if request.method == "POST":
|
| 157 |
+
if User.query.filter_by(username=request.form["username"]).first():
|
| 158 |
+
flash("El nombre de usuario ya existe", "danger")
|
| 159 |
+
return redirect(url_for("register"))
|
| 160 |
+
|
| 161 |
+
user = User(username=request.form["username"])
|
| 162 |
+
user.set_password(request.form["password"])
|
| 163 |
+
db.session.add(user)
|
| 164 |
+
db.session.commit()
|
| 165 |
+
|
| 166 |
+
flash("Registro exitoso. Por favor inicia sesi贸n", "success")
|
| 167 |
+
return redirect(url_for("login"))
|
| 168 |
+
|
| 169 |
+
return render_template("register.html")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.route("/logout")
|
| 173 |
+
@login_required
|
| 174 |
+
def logout():
|
| 175 |
+
logout_user()
|
| 176 |
+
return redirect(url_for("login"))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
if __name__ == "__main__":
|
| 180 |
+
with app.app_context():
|
| 181 |
+
db.create_all()
|
| 182 |
+
app.run(debug=True)
|
config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Config:
|
| 5 |
+
# Configuraci贸n b谩sica
|
| 6 |
+
SECRET_KEY = os.environ.get("SECRET_KEY") or "dev-key-very-secret"
|
| 7 |
+
|
| 8 |
+
# Configuraci贸n de SQLAlchemy
|
| 9 |
+
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///pmbok.db"
|
| 10 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 11 |
+
|
| 12 |
+
# Configuraci贸n de carga de archivos
|
| 13 |
+
UPLOAD_FOLDER = os.path.join(
|
| 14 |
+
os.path.dirname(os.path.abspath(__file__)), "static/uploads"
|
| 15 |
+
)
|
| 16 |
+
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
|
instance/pmbok.db
ADDED
|
Binary file (20.5 kB). View file
|
|
|
models.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 4 |
+
from flask_login import UserMixin
|
| 5 |
+
|
| 6 |
+
db = SQLAlchemy()
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class User(UserMixin, db.Model):
|
| 10 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 11 |
+
username = db.Column(db.String(80), unique=True, nullable=False)
|
| 12 |
+
password_hash = db.Column(db.String(128))
|
| 13 |
+
projects = db.relationship("Project", backref="owner", lazy=True)
|
| 14 |
+
|
| 15 |
+
def set_password(self, password):
|
| 16 |
+
self.password_hash = generate_password_hash(password)
|
| 17 |
+
|
| 18 |
+
def check_password(self, password):
|
| 19 |
+
return check_password_hash(self.password_hash, password)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Project(db.Model):
|
| 23 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 24 |
+
name = db.Column(db.String(100), nullable=False)
|
| 25 |
+
description = db.Column(db.Text)
|
| 26 |
+
start_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
| 27 |
+
end_date = db.Column(db.DateTime)
|
| 28 |
+
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
| 29 |
+
tasks = db.relationship(
|
| 30 |
+
"Task", backref="project", lazy=True, cascade="all, delete-orphan"
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
@property
|
| 34 |
+
def progress(self):
|
| 35 |
+
if not self.tasks:
|
| 36 |
+
return 0
|
| 37 |
+
completed_tasks = sum(1 for task in self.tasks if task.status == "Terminado")
|
| 38 |
+
return round((completed_tasks / len(self.tasks)) * 100, 2)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Task(db.Model):
|
| 42 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 43 |
+
process_name = db.Column(db.String(200), nullable=False)
|
| 44 |
+
process_group = db.Column(db.String(50), nullable=False)
|
| 45 |
+
knowledge_area = db.Column(db.String(50), nullable=False)
|
| 46 |
+
status = db.Column(db.String(20), default="Pendiente")
|
| 47 |
+
evidence_file = db.Column(db.String(255))
|
| 48 |
+
completion_date = db.Column(db.DateTime)
|
| 49 |
+
project_id = db.Column(db.Integer, db.ForeignKey("project.id"), nullable=False)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# Lista de procesos PMBOK 6th Edition
|
| 53 |
+
PMBOK_PROCESSES = [
|
| 54 |
+
# Inicio
|
| 55 |
+
{
|
| 56 |
+
"group": "Inicio",
|
| 57 |
+
"area": "Integraci贸n",
|
| 58 |
+
"name": "Desarrollar Acta de Constituci贸n del Proyecto",
|
| 59 |
+
},
|
| 60 |
+
{"group": "Inicio", "area": "Interesados", "name": "Identificar a los Interesados"},
|
| 61 |
+
# Planificaci贸n
|
| 62 |
+
{
|
| 63 |
+
"group": "Planificaci贸n",
|
| 64 |
+
"area": "Integraci贸n",
|
| 65 |
+
"name": "Desarrollar el Plan para la Direcci贸n del Proyecto",
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"group": "Planificaci贸n",
|
| 69 |
+
"area": "Alcance",
|
| 70 |
+
"name": "Planificar la Gesti贸n del Alcance",
|
| 71 |
+
},
|
| 72 |
+
{"group": "Planificaci贸n", "area": "Alcance", "name": "Recopilar Requisitos"},
|
| 73 |
+
{"group": "Planificaci贸n", "area": "Alcance", "name": "Definir el Alcance"},
|
| 74 |
+
{"group": "Planificaci贸n", "area": "Alcance", "name": "Crear la EDT/WBS"},
|
| 75 |
+
{
|
| 76 |
+
"group": "Planificaci贸n",
|
| 77 |
+
"area": "Tiempo",
|
| 78 |
+
"name": "Planificar la Gesti贸n del Cronograma",
|
| 79 |
+
},
|
| 80 |
+
{"group": "Planificaci贸n", "area": "Tiempo", "name": "Definir las Actividades"},
|
| 81 |
+
{"group": "Planificaci贸n", "area": "Tiempo", "name": "Secuenciar las Actividades"},
|
| 82 |
+
{
|
| 83 |
+
"group": "Planificaci贸n",
|
| 84 |
+
"area": "Tiempo",
|
| 85 |
+
"name": "Estimar la Duraci贸n de las Actividades",
|
| 86 |
+
},
|
| 87 |
+
{"group": "Planificaci贸n", "area": "Tiempo", "name": "Desarrollar el Cronograma"},
|
| 88 |
+
{
|
| 89 |
+
"group": "Planificaci贸n",
|
| 90 |
+
"area": "Costos",
|
| 91 |
+
"name": "Planificar la Gesti贸n de Costos",
|
| 92 |
+
},
|
| 93 |
+
{"group": "Planificaci贸n", "area": "Costos", "name": "Estimar los Costos"},
|
| 94 |
+
{"group": "Planificaci贸n", "area": "Costos", "name": "Determinar el Presupuesto"},
|
| 95 |
+
{
|
| 96 |
+
"group": "Planificaci贸n",
|
| 97 |
+
"area": "Calidad",
|
| 98 |
+
"name": "Planificar la Gesti贸n de Calidad",
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"group": "Planificaci贸n",
|
| 102 |
+
"area": "Recursos Humanos",
|
| 103 |
+
"name": "Planificar la Gesti贸n de Recursos",
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"group": "Planificaci贸n",
|
| 107 |
+
"area": "Comunicaciones",
|
| 108 |
+
"name": "Planificar la Gesti贸n de Comunicaciones",
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"group": "Planificaci贸n",
|
| 112 |
+
"area": "Riesgos",
|
| 113 |
+
"name": "Planificar la Gesti贸n de Riesgos",
|
| 114 |
+
},
|
| 115 |
+
{"group": "Planificaci贸n", "area": "Riesgos", "name": "Identificar los Riesgos"},
|
| 116 |
+
{
|
| 117 |
+
"group": "Planificaci贸n",
|
| 118 |
+
"area": "Riesgos",
|
| 119 |
+
"name": "Realizar An谩lisis Cualitativo de Riesgos",
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"group": "Planificaci贸n",
|
| 123 |
+
"area": "Riesgos",
|
| 124 |
+
"name": "Realizar An谩lisis Cuantitativo de Riesgos",
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"group": "Planificaci贸n",
|
| 128 |
+
"area": "Riesgos",
|
| 129 |
+
"name": "Planificar la Respuesta a los Riesgos",
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"group": "Planificaci贸n",
|
| 133 |
+
"area": "Adquisiciones",
|
| 134 |
+
"name": "Planificar la Gesti贸n de las Adquisiciones",
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"group": "Planificaci贸n",
|
| 138 |
+
"area": "Interesados",
|
| 139 |
+
"name": "Planificar la Participaci贸n de los Interesados",
|
| 140 |
+
},
|
| 141 |
+
# Ejecuci贸n
|
| 142 |
+
{
|
| 143 |
+
"group": "Ejecuci贸n",
|
| 144 |
+
"area": "Integraci贸n",
|
| 145 |
+
"name": "Dirigir y Gestionar el Trabajo del Proyecto",
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"group": "Ejecuci贸n",
|
| 149 |
+
"area": "Integraci贸n",
|
| 150 |
+
"name": "Gestionar el Conocimiento del Proyecto",
|
| 151 |
+
},
|
| 152 |
+
{"group": "Ejecuci贸n", "area": "Calidad", "name": "Gestionar la Calidad"},
|
| 153 |
+
{"group": "Ejecuci贸n", "area": "Recursos Humanos", "name": "Adquirir Recursos"},
|
| 154 |
+
{"group": "Ejecuci贸n", "area": "Recursos Humanos", "name": "Desarrollar el Equipo"},
|
| 155 |
+
{"group": "Ejecuci贸n", "area": "Recursos Humanos", "name": "Dirigir el Equipo"},
|
| 156 |
+
{
|
| 157 |
+
"group": "Ejecuci贸n",
|
| 158 |
+
"area": "Comunicaciones",
|
| 159 |
+
"name": "Gestionar las Comunicaciones",
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"group": "Ejecuci贸n",
|
| 163 |
+
"area": "Riesgos",
|
| 164 |
+
"name": "Implementar la Respuesta a los Riesgos",
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"group": "Ejecuci贸n",
|
| 168 |
+
"area": "Adquisiciones",
|
| 169 |
+
"name": "Efectuar las Adquisiciones",
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"group": "Ejecuci贸n",
|
| 173 |
+
"area": "Interesados",
|
| 174 |
+
"name": "Gestionar la Participaci贸n de los Interesados",
|
| 175 |
+
},
|
| 176 |
+
# Monitoreo y Control
|
| 177 |
+
{
|
| 178 |
+
"group": "Monitoreo",
|
| 179 |
+
"area": "Integraci贸n",
|
| 180 |
+
"name": "Monitorear y Controlar el Trabajo del Proyecto",
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"group": "Monitoreo",
|
| 184 |
+
"area": "Integraci贸n",
|
| 185 |
+
"name": "Realizar el Control Integrado de Cambios",
|
| 186 |
+
},
|
| 187 |
+
{"group": "Monitoreo", "area": "Alcance", "name": "Validar el Alcance"},
|
| 188 |
+
{"group": "Monitoreo", "area": "Alcance", "name": "Controlar el Alcance"},
|
| 189 |
+
{"group": "Monitoreo", "area": "Tiempo", "name": "Controlar el Cronograma"},
|
| 190 |
+
{"group": "Monitoreo", "area": "Costos", "name": "Controlar los Costos"},
|
| 191 |
+
{"group": "Monitoreo", "area": "Calidad", "name": "Controlar la Calidad"},
|
| 192 |
+
{
|
| 193 |
+
"group": "Monitoreo",
|
| 194 |
+
"area": "Recursos Humanos",
|
| 195 |
+
"name": "Controlar los Recursos",
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"group": "Monitoreo",
|
| 199 |
+
"area": "Comunicaciones",
|
| 200 |
+
"name": "Monitorear las Comunicaciones",
|
| 201 |
+
},
|
| 202 |
+
{"group": "Monitoreo", "area": "Riesgos", "name": "Monitorear los Riesgos"},
|
| 203 |
+
{
|
| 204 |
+
"group": "Monitoreo",
|
| 205 |
+
"area": "Adquisiciones",
|
| 206 |
+
"name": "Controlar las Adquisiciones",
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"group": "Monitoreo",
|
| 210 |
+
"area": "Interesados",
|
| 211 |
+
"name": "Monitorear la Participaci贸n de los Interesados",
|
| 212 |
+
},
|
| 213 |
+
# Cierre
|
| 214 |
+
{"group": "Cierre", "area": "Integraci贸n", "name": "Cerrar el Proyecto o Fase"},
|
| 215 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
Flask-SQLAlchemy==3.1.1
|
| 3 |
+
Flask-Login==0.6.3
|
| 4 |
+
Werkzeug==3.0.1
|
| 5 |
+
python-dotenv==1.0.0
|
static/uploads/1_mermaid-diagram-2025-06-25-183414.png
ADDED
|
Git LFS Details
|
templates/base.html
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="es">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>{% block title %}PMBOK Tracker{% endblock %}</title>
|
| 8 |
+
<!-- Tailwind CSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<!-- Bootstrap CSS -->
|
| 11 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 12 |
+
</head>
|
| 13 |
+
|
| 14 |
+
<body class="bg-gray-100">
|
| 15 |
+
<nav class="bg-blue-600 text-white shadow-lg mb-4">
|
| 16 |
+
<div class="container mx-auto px-4 py-3">
|
| 17 |
+
<div class="flex justify-between items-center">
|
| 18 |
+
<a href="{{ url_for('index') }}" class="text-xl font-bold">PMBOK Tracker</a>
|
| 19 |
+
<div>
|
| 20 |
+
{% if current_user.is_authenticated %}
|
| 21 |
+
<span class="mr-4">{{ current_user.username }}</span>
|
| 22 |
+
<a href="{{ url_for('logout') }}" class="text-white hover:text-gray-200">Cerrar Sesi贸n</a>
|
| 23 |
+
{% else %}
|
| 24 |
+
<a href="{{ url_for('login') }}" class="text-white hover:text-gray-200">Iniciar Sesi贸n</a>
|
| 25 |
+
{% endif %}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</nav>
|
| 30 |
+
|
| 31 |
+
<div class="container mx-auto px-4">
|
| 32 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 33 |
+
{% if messages %}
|
| 34 |
+
{% for category, message in messages %}
|
| 35 |
+
<div class="alert alert-{{ category }} mb-4">
|
| 36 |
+
{{ message }}
|
| 37 |
+
</div>
|
| 38 |
+
{% endfor %}
|
| 39 |
+
{% endif %}
|
| 40 |
+
{% endwith %}
|
| 41 |
+
|
| 42 |
+
{% block content %}{% endblock %}
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<!-- Bootstrap JS Bundle -->
|
| 46 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
| 47 |
+
</body>
|
| 48 |
+
|
| 49 |
+
</html>
|
templates/create_project.html
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
| 5 |
+
<div class="flex justify-between items-center mb-6">
|
| 6 |
+
<h2 class="text-2xl font-bold text-gray-800">Crear Nuevo Proyecto</h2>
|
| 7 |
+
<a href="{{ url_for('index') }}" class="btn btn-secondary">Volver</a>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
<form method="POST" action="{{ url_for('create_project') }}">
|
| 11 |
+
<div class="mb-4">
|
| 12 |
+
<label for="name" class="block text-gray-700 text-sm font-bold mb-2">Nombre del Proyecto</label>
|
| 13 |
+
<input type="text" id="name" name="name" required class="form-control"
|
| 14 |
+
placeholder="Ingresa el nombre del proyecto">
|
| 15 |
+
</div>
|
| 16 |
+
|
| 17 |
+
<div class="mb-4">
|
| 18 |
+
<label for="description" class="block text-gray-700 text-sm font-bold mb-2">Descripci贸n</label>
|
| 19 |
+
<textarea id="description" name="description" rows="4" class="form-control"
|
| 20 |
+
placeholder="Describe el proyecto"></textarea>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div class="mb-4">
|
| 24 |
+
<label for="start_date" class="block text-gray-700 text-sm font-bold mb-2">Fecha de Inicio</label>
|
| 25 |
+
<input type="date" id="start_date" name="start_date" required class="form-control">
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="mb-6">
|
| 29 |
+
<label for="end_date" class="block text-gray-700 text-sm font-bold mb-2">Fecha de Fin (Opcional)</label>
|
| 30 |
+
<input type="date" id="end_date" name="end_date" class="form-control">
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="flex justify-end">
|
| 34 |
+
<button type="submit" class="btn btn-primary">
|
| 35 |
+
Crear Proyecto
|
| 36 |
+
</button>
|
| 37 |
+
</div>
|
| 38 |
+
</form>
|
| 39 |
+
</div>
|
| 40 |
+
{% endblock %}
|
templates/index.html
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
|
| 5 |
+
<div class="flex justify-between items-center mb-6">
|
| 6 |
+
<h1 class="text-2xl font-bold text-gray-800">Proyectos PMBOK</h1>
|
| 7 |
+
<a href="{{ url_for('create_project') }}" class="btn btn-primary">Nuevo Proyecto</a>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
{% if projects %}
|
| 11 |
+
<div class="overflow-x-auto">
|
| 12 |
+
<table class="table table-striped">
|
| 13 |
+
<thead class="bg-gray-50">
|
| 14 |
+
<tr>
|
| 15 |
+
<th scope="col">Nombre</th>
|
| 16 |
+
<th scope="col">Descripci贸n</th>
|
| 17 |
+
<th scope="col">Fecha Inicio</th>
|
| 18 |
+
<th scope="col">Fecha Fin</th>
|
| 19 |
+
<th scope="col">Progreso</th>
|
| 20 |
+
<th scope="col">Acciones</th>
|
| 21 |
+
</tr>
|
| 22 |
+
</thead>
|
| 23 |
+
<tbody>
|
| 24 |
+
{% for project in projects %}
|
| 25 |
+
<tr>
|
| 26 |
+
<td>{{ project.name }}</td>
|
| 27 |
+
<td>{{ project.description[:100] }}...</td>
|
| 28 |
+
<td>{{ project.start_date.strftime('%d/%m/%Y') }}</td>
|
| 29 |
+
<td>{{ project.end_date.strftime('%d/%m/%Y') if project.end_date else 'No definida' }}</td>
|
| 30 |
+
<td>
|
| 31 |
+
<div class="progress">
|
| 32 |
+
<div class="progress-bar" role="progressbar" style="width: {{ project.progress }}%"
|
| 33 |
+
aria-valuenow="{{ project.progress }}" aria-valuemin="0" aria-valuemax="100">
|
| 34 |
+
{{ project.progress }}%
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</td>
|
| 38 |
+
<td>
|
| 39 |
+
<a href="{{ url_for('project_detail', project_id=project.id) }}" class="btn btn-sm btn-info">Ver
|
| 40 |
+
Detalle</a>
|
| 41 |
+
</td>
|
| 42 |
+
</tr>
|
| 43 |
+
{% endfor %}
|
| 44 |
+
</tbody>
|
| 45 |
+
</table>
|
| 46 |
+
</div>
|
| 47 |
+
{% else %}
|
| 48 |
+
<div class="text-center py-6">
|
| 49 |
+
<p class="text-gray-600">No hay proyectos registrados.</p>
|
| 50 |
+
<a href="{{ url_for('create_project') }}" class="btn btn-primary mt-4">Crear Primer Proyecto</a>
|
| 51 |
+
</div>
|
| 52 |
+
{% endif %}
|
| 53 |
+
</div>
|
| 54 |
+
{% endblock %}
|
templates/login.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
| 5 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">Iniciar Sesi贸n</h2>
|
| 6 |
+
|
| 7 |
+
<form method="POST" action="{{ url_for('login') }}">
|
| 8 |
+
<div class="mb-4">
|
| 9 |
+
<label for="username" class="block text-gray-700 text-sm font-bold mb-2">Usuario</label>
|
| 10 |
+
<input type="text" id="username" name="username" required class="form-control"
|
| 11 |
+
placeholder="Ingresa tu nombre de usuario">
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<div class="mb-6">
|
| 15 |
+
<label for="password" class="block text-gray-700 text-sm font-bold mb-2">Contrase帽a</label>
|
| 16 |
+
<input type="password" id="password" name="password" required class="form-control"
|
| 17 |
+
placeholder="Ingresa tu contrase帽a">
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div class="flex items-center justify-between">
|
| 21 |
+
<button type="submit" class="btn btn-primary w-full">
|
| 22 |
+
Iniciar Sesi贸n
|
| 23 |
+
</button>
|
| 24 |
+
</div>
|
| 25 |
+
</form>
|
| 26 |
+
|
| 27 |
+
<div class="mt-4 text-center">
|
| 28 |
+
<p class="text-gray-600">驴No tienes una cuenta?
|
| 29 |
+
<a href="{{ url_for('register') }}" class="text-blue-500 hover:text-blue-700">
|
| 30 |
+
Reg铆strate
|
| 31 |
+
</a>
|
| 32 |
+
</p>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
{% endblock %}
|
templates/project.html
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="bg-white rounded-lg shadow-md p-6">
|
| 5 |
+
<div class="mb-6">
|
| 6 |
+
<div class="flex justify-between items-center">
|
| 7 |
+
<h1 class="text-2xl font-bold text-gray-800">{{ project.name }}</h1>
|
| 8 |
+
<a href="{{ url_for('index') }}" class="btn btn-secondary">Volver a Proyectos</a>
|
| 9 |
+
</div>
|
| 10 |
+
<p class="text-gray-600 mt-2">{{ project.description }}</p>
|
| 11 |
+
<div class="mt-4">
|
| 12 |
+
<strong>Progreso General:</strong>
|
| 13 |
+
<div class="progress mt-2">
|
| 14 |
+
<div class="progress-bar" role="progressbar" style="width: {{ project.progress }}%"
|
| 15 |
+
aria-valuenow="{{ project.progress }}" aria-valuemin="0" aria-valuemax="100">
|
| 16 |
+
{{ project.progress }}%
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="mb-4">
|
| 23 |
+
<div class="btn-group" role="group">
|
| 24 |
+
{% for group in ["Inicio", "Planificaci贸n", "Ejecuci贸n", "Monitoreo", "Cierre"] %}
|
| 25 |
+
<button type="button" class="btn btn-outline-primary filter-btn" data-group="{{ group }}">
|
| 26 |
+
{{ group }}
|
| 27 |
+
</button>
|
| 28 |
+
{% endfor %}
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<div class="overflow-x-auto">
|
| 33 |
+
<table class="table table-striped">
|
| 34 |
+
<thead class="bg-gray-50">
|
| 35 |
+
<tr>
|
| 36 |
+
<th>Proceso</th>
|
| 37 |
+
<th>Grupo</th>
|
| 38 |
+
<th>脕rea</th>
|
| 39 |
+
<th>Estado</th>
|
| 40 |
+
<th>Evidencia</th>
|
| 41 |
+
<th>Acciones</th>
|
| 42 |
+
</tr>
|
| 43 |
+
</thead>
|
| 44 |
+
<tbody>
|
| 45 |
+
{% for task in project.tasks %}
|
| 46 |
+
<tr class="task-row" data-group="{{ task.process_group }}">
|
| 47 |
+
<td>{{ task.process_name }}</td>
|
| 48 |
+
<td>{{ task.process_group }}</td>
|
| 49 |
+
<td>{{ task.knowledge_area }}</td>
|
| 50 |
+
<td>
|
| 51 |
+
<span
|
| 52 |
+
class="badge {% if task.status == 'Terminado' %}bg-success{% elif task.status == 'En Proceso' %}bg-warning{% else %}bg-secondary{% endif %}">
|
| 53 |
+
{{ task.status }}
|
| 54 |
+
</span>
|
| 55 |
+
</td>
|
| 56 |
+
<td>
|
| 57 |
+
{% if task.evidence_file %}
|
| 58 |
+
<a href="{{ url_for('download_file', filename=task.evidence_file) }}"
|
| 59 |
+
class="btn btn-sm btn-link">
|
| 60 |
+
Descargar
|
| 61 |
+
</a>
|
| 62 |
+
{% else %}
|
| 63 |
+
<span class="text-muted">Sin evidencia</span>
|
| 64 |
+
{% endif %}
|
| 65 |
+
</td>
|
| 66 |
+
<td>
|
| 67 |
+
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal"
|
| 68 |
+
data-bs-target="#taskModal{{ task.id }}">
|
| 69 |
+
Actualizar
|
| 70 |
+
</button>
|
| 71 |
+
|
| 72 |
+
<!-- Modal para cada tarea -->
|
| 73 |
+
<div class="modal fade" id="taskModal{{ task.id }}" tabindex="-1">
|
| 74 |
+
<div class="modal-dialog">
|
| 75 |
+
<div class="modal-content">
|
| 76 |
+
<div class="modal-header">
|
| 77 |
+
<h5 class="modal-title">Actualizar Tarea</h5>
|
| 78 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 79 |
+
</div>
|
| 80 |
+
<form action="{{ url_for('update_task', task_id=task.id) }}" method="POST"
|
| 81 |
+
enctype="multipart/form-data">
|
| 82 |
+
<div class="modal-body">
|
| 83 |
+
<div class="mb-3">
|
| 84 |
+
<label class="form-label">Estado</label>
|
| 85 |
+
<select name="status" class="form-select" required>
|
| 86 |
+
<option value="Pendiente" {% if task.status=='Pendiente'
|
| 87 |
+
%}selected{% endif %}>Pendiente</option>
|
| 88 |
+
<option value="En Proceso" {% if task.status=='En Proceso'
|
| 89 |
+
%}selected{% endif %}>En Proceso</option>
|
| 90 |
+
<option value="Terminado" {% if task.status=='Terminado'
|
| 91 |
+
%}selected{% endif %}>Terminado</option>
|
| 92 |
+
</select>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="mb-3">
|
| 95 |
+
<label class="form-label">Evidencia (PDF, DOCX, Imagen)</label>
|
| 96 |
+
<input type="file" name="evidence" class="form-control"
|
| 97 |
+
accept=".pdf,.docx,.jpg,.jpeg,.png">
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
<div class="modal-footer">
|
| 101 |
+
<button type="button" class="btn btn-secondary"
|
| 102 |
+
data-bs-dismiss="modal">Cancelar</button>
|
| 103 |
+
<button type="submit" class="btn btn-primary">Guardar</button>
|
| 104 |
+
</div>
|
| 105 |
+
</form>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</td>
|
| 110 |
+
</tr>
|
| 111 |
+
{% endfor %}
|
| 112 |
+
</tbody>
|
| 113 |
+
</table>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<script>
|
| 118 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 119 |
+
const filterButtons = document.querySelectorAll('.filter-btn');
|
| 120 |
+
const taskRows = document.querySelectorAll('.task-row');
|
| 121 |
+
|
| 122 |
+
filterButtons.forEach(button => {
|
| 123 |
+
button.addEventListener('click', function () {
|
| 124 |
+
const group = this.dataset.group;
|
| 125 |
+
|
| 126 |
+
// Toggle active state of buttons
|
| 127 |
+
filterButtons.forEach(btn => btn.classList.remove('active'));
|
| 128 |
+
this.classList.add('active');
|
| 129 |
+
|
| 130 |
+
// Filter rows
|
| 131 |
+
taskRows.forEach(row => {
|
| 132 |
+
if (group === 'all' || row.dataset.group === group) {
|
| 133 |
+
row.style.display = '';
|
| 134 |
+
} else {
|
| 135 |
+
row.style.display = 'none';
|
| 136 |
+
}
|
| 137 |
+
});
|
| 138 |
+
});
|
| 139 |
+
});
|
| 140 |
+
});
|
| 141 |
+
</script>
|
| 142 |
+
{% endblock %}
|
templates/register.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block content %}
|
| 4 |
+
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
| 5 |
+
<h2 class="text-2xl font-bold text-gray-800 mb-6 text-center">Registro</h2>
|
| 6 |
+
|
| 7 |
+
<form method="POST" action="{{ url_for('register') }}">
|
| 8 |
+
<div class="mb-4">
|
| 9 |
+
<label for="username" class="block text-gray-700 text-sm font-bold mb-2">Usuario</label>
|
| 10 |
+
<input type="text" id="username" name="username" required class="form-control"
|
| 11 |
+
placeholder="Elige un nombre de usuario">
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<div class="mb-6">
|
| 15 |
+
<label for="password" class="block text-gray-700 text-sm font-bold mb-2">Contrase帽a</label>
|
| 16 |
+
<input type="password" id="password" name="password" required class="form-control"
|
| 17 |
+
placeholder="Elige una contrase帽a segura">
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div class="flex items-center justify-between">
|
| 21 |
+
<button type="submit" class="btn btn-primary w-full">
|
| 22 |
+
Registrarse
|
| 23 |
+
</button>
|
| 24 |
+
</div>
|
| 25 |
+
</form>
|
| 26 |
+
|
| 27 |
+
<div class="mt-4 text-center">
|
| 28 |
+
<p class="text-gray-600">驴Ya tienes una cuenta?
|
| 29 |
+
<a href="{{ url_for('login') }}" class="text-blue-500 hover:text-blue-700">
|
| 30 |
+
Inicia Sesi贸n
|
| 31 |
+
</a>
|
| 32 |
+
</p>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
{% endblock %}
|