diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a8c2003 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/admin/__init__.py b/admin/__init__.py new file mode 100644 index 0000000..47b9dc7 --- /dev/null +++ b/admin/__init__.py @@ -0,0 +1,16 @@ +from flask_admin import Admin +from flask_admin.contrib.sqla import ModelView +from models import ( + Usuario, + Taquilla, + Alquiler, + Seccion, + db +) + +admin = Admin() + +admin.add_view(ModelView(Usuario, db.session)) +admin.add_view(ModelView(Taquilla, db.session)) +admin.add_view(ModelView(Seccion, db.session)) +admin.add_view(ModelView(Alquiler, db.session)) diff --git a/endpoints/alquileres.py b/endpoints/alquileres.py new file mode 100644 index 0000000..3cfc9a7 --- /dev/null +++ b/endpoints/alquileres.py @@ -0,0 +1,141 @@ +from flask import ( + Blueprint, + render_template, + request, + redirect +) +from models import ( + Alquiler, + Usuario, + Taquilla, + db +) +import datetime +from dateutil.relativedelta import relativedelta + +alquileres = Blueprint( + "gestion_alquileres", + __name__ +) + +@alquileres.route("/ver-alquileres") +def ver_alquileres(info = None): + alquileres = ( + db.session.query( + Usuario.id, + Usuario.nombre, + Usuario.dni, + Taquilla.id, + Taquilla.id_taquilla, + Alquiler.validez + ) + .join(Alquiler, Alquiler.id_usuario == Usuario.id) + .join(Taquilla, Alquiler.id_taquilla == Taquilla.id) + .all() + ) + + alquileres = [ + { + "id_usuario": id_usuario, + "id_taquilla": id_taquilla, + "nombre_persona": nombre, + "dni_persona": dni, + "taquilla": numero_taquilla, + "validez": datetime.datetime.fromtimestamp(int(validez)).strftime("%d/%m/%Y %H:%M:%S") + } for + id_usuario, + nombre, + dni, + id_taquilla, + numero_taquilla, + validez + in alquileres + ] + + return render_template("ver_alquileres.html", alquileres=alquileres, info=info) + +@alquileres.route("/renovar/") +def renovar_alquiler(id_taquilla): + if not id_taquilla: + return redirect("/") + + alquiler = ( + db.session.query(Alquiler) + .filter(Alquiler.id_taquilla == id_taquilla) + .first() + ) + + current_date = datetime.datetime.now() + validez = current_date + relativedelta(months=3) + validez = validez.replace(hour=23, minute=59, second=0, microsecond=0) + validez = int(validez.timestamp()) + + alquiler.validez = validez + + db.session.commit() + + usuario = ( + db.session.query(Usuario) + .filter(Usuario.id == alquiler.id_usuario) + .first() + ) + + return ver_alquileres(info=f"Alquiler renovado correctamente para {usuario.nombre}") + +@alquileres.route("/confirmar-baja/") +def confirmar_baja(id_taquilla): + info_taquilla = ( + db.session.query( + Taquilla.id_taquilla, + Usuario.nombre + ) + .join(Alquiler, Alquiler.id_taquilla == Taquilla.id) + .join(Usuario, Alquiler.id_usuario == Usuario.id) + .filter(Taquilla.id == id_taquilla) + .first() + ) + + info_taquilla = { + "numero_taquilla": info_taquilla[0], + "nombre_persona": info_taquilla[1] + } + + return render_template( + "confirmar_baja.html", + info_taquilla=info_taquilla, + id_taquilla=id_taquilla + ) + +@alquileres.route("/baja/") +def dar_baja(id_taquilla): + alquiler = ( + db.session.query(Alquiler) + .filter(Alquiler.id_taquilla == id_taquilla) + .first() + ) + + usuario = ( + db.session.query(Usuario) + .filter(Usuario.id == alquiler.id_usuario) + .first() + ) + + taquilla = ( + db.session.query(Taquilla) + .filter(Taquilla.id == id_taquilla) + .first() + ) + + if alquiler and usuario and taquilla: + nombre_usuario = usuario.nombre + id_taquilla = taquilla.id_taquilla + + db.session.delete(alquiler) + db.session.delete(usuario) + + db.session.commit() + + return ver_alquileres(info=f"Se ha dado de baja el alquiler para {nombre_usuario} en la taquilla {id_taquilla}") + + else: + return ver_alquileres(info="No existe este alquiler") \ No newline at end of file diff --git a/endpoints/retrasos.py b/endpoints/retrasos.py new file mode 100644 index 0000000..66ff960 --- /dev/null +++ b/endpoints/retrasos.py @@ -0,0 +1,61 @@ +from flask import ( + Blueprint, + render_template +) +from models import ( + Usuario, + Taquilla, + Alquiler, + db +) +import datetime + + +retrasos = Blueprint( + "gestion_retrasos", + __name__ +) + +@retrasos.route("/ver-retrasos") +def ver_retrasos(info = None): + current_date = int(datetime.datetime.now().timestamp()) + + alquileres = ( + db.session.query( + Usuario.id, + Usuario.nombre, + Usuario.dni, + Taquilla.id, + Taquilla.id_taquilla, + Alquiler.validez + ) + .join(Alquiler, Alquiler.id_usuario == Usuario.id) + .join(Taquilla, Alquiler.id_taquilla == Taquilla.id) + .filter(Alquiler.validez < current_date) + .all() + ) + + alquileres = [ + { + "id_usuario": id_usuario, + "id_taquilla": id_taquilla, + "nombre_persona": nombre, + "dni_persona": dni, + "taquilla": numero_taquilla, + "validez": datetime.datetime.fromtimestamp(int(validez)).strftime("%d/%m/%Y %H:%M:%S") + } for + id_usuario, + nombre, + dni, + id_taquilla, + numero_taquilla, + validez + in alquileres + ] + + if len(alquileres) > 0: + return render_template("ver_alquileres.html", alquileres=alquileres, info=info) + + else: + return render_template("ver_alquileres.html", alquileres=alquileres, info="No existe ningún alquiler expirado.") + \ No newline at end of file diff --git a/endpoints/taquillas.py b/endpoints/taquillas.py new file mode 100644 index 0000000..96f83c5 --- /dev/null +++ b/endpoints/taquillas.py @@ -0,0 +1,138 @@ +from flask import ( + Blueprint, + render_template, + request +) +from models import ( + Seccion, + Taquilla, + Alquiler, + Usuario, + db +) +import time +import datetime +from dateutil.relativedelta import relativedelta + +taquillas = Blueprint( + "gestion_taquillas", + __name__ +) + +@taquillas.route("/secciones") +def ver_secciones(): + secciones = db.session.query( + Seccion + ) + + return render_template( + "ver_secciones.html", + secciones=secciones + ) + +@taquillas.route("/secciones/") +def ver_taquillas_seccion(id_seccion): + taquillas = ( + db.session.query(Taquilla.id_taquilla, Alquiler.id_usuario) + .join(Alquiler, Taquilla.id == Alquiler.id_taquilla, isouter=True) + .filter(Taquilla.id_seccion == int(id_seccion)) + .all() + ) + + print(taquillas) + + taquillas_result = [ + { + "id_taquilla": id_taquilla, + "alquilado": id_usuario is not None + } for id_taquilla, id_usuario in taquillas + ] + + return render_template( + "ver_taquillas.html", + taquillas=taquillas_result + ) + +@taquillas.route("/alquilar/", methods=["GET", "POST"]) +def alquilar_taquilla(id_taquilla): + if request.method == "GET": + return render_template( + "alquilar_taquilla.html", + id_taquilla=id_taquilla + ) + + else: + nombre = request.form.get("nombre_usuario").upper() + dni = request.form.get("dni_usuario").upper() + correo = request.form.get("correo_electronico") + agreement = request.form.get("agreement") + + print(nombre, dni, correo, agreement) + error = None + + if not (dni and correo and nombre): + error = "Faltan datos." + + if not agreement: + error = "No está de acuerdo con las políticas." + + user = ( + db.session.query(Usuario) + .filter(Usuario.dni == dni) + .first() + ) + + if not user: + user = Usuario() + user.correo = correo + user.dni = dni + user.nombre = nombre + + db.session.add(user) + + alquiler_user = ( + db.session.query(Alquiler) + .filter(Alquiler.id_usuario == user.id) + .first() + ) + + if alquiler_user: + error = "Usuario ya tiene una taquilla alquilada. Debe renovar o dar de baja el alquiler." + + taquilla = ( + db.session.query(Taquilla) + .filter(Taquilla.id_taquilla == id_taquilla) + .first() + ) + + if not taquilla: + error = "La taquilla en cuestión no existe" + + if not error: + alquiler_user = Alquiler() + alquiler_user.id_usuario = user.id + alquiler_user.id_taquilla = taquilla.id + + current_date = datetime.datetime.now() + validez = current_date + relativedelta(months=3) + validez = validez.replace(hour=23, minute=59, second=0, microsecond=0) + + alquiler_user.validez = int(validez.timestamp()) + + db.session.add(alquiler_user) + + db.session.commit() + + return render_template( + "pagina_principal.html", + info=f"¡La taquilla {id_taquilla} ha sido alquilada a {nombre}!" + ) + + else: + print(error) + return render_template( + "alquilar_taquilla.html", + id_taquilla=id_taquilla, + info=error + ) + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f86992b --- /dev/null +++ b/main.py @@ -0,0 +1,38 @@ +from flask import ( + Flask, + render_template +) + +from models import db +from admin import admin +from endpoints.taquillas import taquillas +from endpoints.alquileres import alquileres +from endpoints.retrasos import retrasos + +app = Flask(__name__) + +app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///project.db" +app.secret_key = "hola" + +db.init_app(app) + +app.register_blueprint(taquillas) +app.register_blueprint(alquileres) +app.register_blueprint(retrasos) + +@app.route("/") +def index(): + return render_template("pagina_principal.html") + + +with app.app_context(): + db.create_all() + + +if __name__ == "__main__": + admin.init_app(app) + app.run( + host="0.0.0.0", + port=5000, + debug=True + ) \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..f3fcc4d --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,42 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + + +class Seccion(db.Model): + __tablename__ = "seccion" + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String) + descripcion = db.Column(db.String) + + +class Taquilla(db.Model): + __tablename__ = "taquilla" + id = db.Column(db.Integer, primary_key=True) + id_taquilla = db.Column(db.Integer) + id_seccion = db.Column(db.Integer, db.ForeignKey("seccion.id")) + + +class Usuario(db.Model): + __tablename__ = "usuario" + id = db.Column(db.Integer, primary_key=True) + nombre = db.Column(db.String) + dni = db.Column(db.String) + correo = db.Column(db.String) + + +class Alquiler(db.Model): + __tablename__ = "alquiler" + id_taquilla = db.Column( + db.Integer, + db.ForeignKey("taquilla.id"), + primary_key=True + ) + id_usuario = db.Column( + db.Integer, + db.ForeignKey("usuario.id"), + primary_key=True + ) + validez = db.Column( + db.Integer + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c324778 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask +flask-login +flask-admin +flask-sqlalchemy +python-dateutil \ No newline at end of file diff --git a/static/index.css b/static/index.css new file mode 100644 index 0000000..b02d6d2 --- /dev/null +++ b/static/index.css @@ -0,0 +1,181 @@ +h1, a, p, label { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; +} + +html, body { + margin: 0; + padding: 0; + height: 100%; +} + +nav { + background-color: rgb(32, 32, 90); + width: calc(100% - 8px); + height: 24px; + display: flex; + align-items: center; + padding: 4px; + border-radius: 0px 0px 4px 4px; +} + +a.title, a.title:active { + color: white; + text-decoration: none; +} + +.nav-items { + display: flex; + align-items: center; + gap: 8px; +} + +.content { + margin: 8px; + overflow-x: auto; + overflow-y: hidden; + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + justify-content: center; +} + +.slider { + display: flex; + gap: 8px; + height: 100%; + flex-shrink: 0; + gap: 4px; + flex-wrap: wrap; +} + +.slider-item { + border-radius: 4px; + display: flex; + flex-direction: column; + width: 256px; + height: 256px; + border: 1px solid gray; + align-items: center; + justify-content: center; + text-align: center; +} + +.grid { + display: flex; + gap: 8px; + height: 100%; + overflow-y: auto; + flex-wrap: wrap; + align-content: flex-start; + justify-content: center; +} + +.grid-item { + border-radius: 4px; + display: flex; + flex-direction: column; + width: 128px; + height: 64px; + border: 1px solid gray; + align-items: center; + justify-content: center; + text-align: center; +} + +.grid-item:hover, .slider-item:hover, .disabled { + background-color: rgb(128, 128, 128); +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; + background-color: #e0e0e0; + border-radius: 8px; + padding: 16px; + border: 1px solid #e1e1e1; +} + +input[type=text] { + padding: 8px; + font-size: 24px; + border-radius: 8px; + border: 0px +} + +.form-element { + display: flex; + flex-direction: column; +} + +.form-content { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.button { + padding: 8px; + font-size: 16px; + border-radius: 8px; + border: 0px; + background-color: #c0c0c0; +} + +.form-submit { + display: flex; + justify-content: space-between; + width: 100%; +} + +.info { + padding: 8px; + background-color: #bbccff; + border-radius: 8px; + border: 2px solid #99aaff; + margin: 8px; +} + +table { + /* Box Model & Layout */ + width: 100%; /* Make the table take up the full width of its container */ + border-collapse: collapse; /* Essential: Merges the borders of adjacent cells into a single line */ + font-family: Arial, sans-serif; /* A clean, readable font */ + box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); /* Subtle shadow for depth */ +} + +/* Styling the Table Header */ +th { + background-color: #007bff; /* Primary color background for the header */ + color: white; /* White text for contrast */ + text-align: left; /* Align text to the left */ + padding: 12px 15px; /* Comfortable padding */ +} + +/* Styling the Table Cells (Data and Headers) */ +td, th { + border: 1px solid #dddddd; /* Light gray border for all cells */ + padding: 10px 15px; /* Consistent padding */ +} + +/* Styling the Table Rows */ +tr:nth-child(even) { + background-color: #f3f3f3; /* Zebra striping: light gray background for even rows */ +} + +tr:hover { + background-color: #e0e0e0; /* Highlight the row when the user hovers over it */ + cursor: pointer; /* Change cursor to pointer to indicate interactivity */ +} + +/* Optional: Styling the Table Footer (if you use a element) */ +tfoot td { + font-weight: bold; + background-color: #cccccc; +} + +a { + color: blue +} \ No newline at end of file diff --git a/templates/alquilar_taquilla.html b/templates/alquilar_taquilla.html new file mode 100644 index 0000000..cd902af --- /dev/null +++ b/templates/alquilar_taquilla.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} + +{% block title %} +Alquilar taquilla +{% endblock %} + +{% block content %} +
+ +
+
+ {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c283f16 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,26 @@ + + + + NacaTaquillas | {% block title %}{% endblock %} + + + + + + + {% if info %} +
+ {{ info }} +
+ {% endif %} + + {% block content %} + {% endblock %} + + + {% block script %} + + {% endblock %} + diff --git a/templates/confirmar_baja.html b/templates/confirmar_baja.html new file mode 100644 index 0000000..dafa1ce --- /dev/null +++ b/templates/confirmar_baja.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block index %} +Confirmar baja +{% endblock %} + +{% block content %} +
+ +
+
+

¿Estás seguro de que quieres dar de baja la taquilla {{ info_taquilla.numero_taquilla }}, asociada a {{ info_taquilla.nombre_persona }}?

+
+ + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/pagina_principal.html b/templates/pagina_principal.html new file mode 100644 index 0000000..edd82d0 --- /dev/null +++ b/templates/pagina_principal.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %} +Inicio +{% endblock %} + +{% block content %} +
+ + +
+
+

Alquilar taquilla

+
+
+

Alquilar una taquilla a un usuario

+
+
+ +
+

Ver alquileres

+

Ver las taquillas alquiladas en una sección

+
+ +
+

Ver alquileres caducados

+

Ver los alquileres que no se han renovado hasta la fecha actual

+
+ +
+

Administración

+

¡¡Funciones de administrador!!

+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/ver_alquileres.html b/templates/ver_alquileres.html new file mode 100644 index 0000000..3969869 --- /dev/null +++ b/templates/ver_alquileres.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block title %} +Alquileres +{% endblock %} + +{% block content %} + + + + + + + + + {% for alquiler in alquileres %} + + + + + + + + + {% endfor %} + +
ID de taquillaNombre de usuarioDNIValidezAcciones
{{ alquiler.taquilla }}{{ alquiler.nombre_persona }}{{ alquiler.dni_persona }}{{ alquiler.validez }} + Renovar + Baja +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/ver_secciones.html b/templates/ver_secciones.html new file mode 100644 index 0000000..dc1c624 --- /dev/null +++ b/templates/ver_secciones.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %} +Secciones +{% endblock %} + +{% block content %} +
+ {% for seccion in secciones %} +
+

{{ seccion.nombre }}

+

{{ seccion.descripcion }}

+
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/ver_taquillas.html b/templates/ver_taquillas.html new file mode 100644 index 0000000..47110bc --- /dev/null +++ b/templates/ver_taquillas.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block title %} +Taquillas +{% endblock %} + +{% block content %} +
+
+ {% for taquilla in taquillas %} + {% set desactivado = "disabled" if taquilla.alquilado else "" %} +
+

{{ taquilla.id_taquilla }}

+
+ {% endfor %} +
+
+{% endblock %}