diff --git a/Documents_ref/FORMHa Web_V2.pptx b/Documents_ref/FORMHa Web_V2.pptx index caa88b7..543171b 100644 Binary files a/Documents_ref/FORMHa Web_V2.pptx and b/Documents_ref/FORMHa Web_V2.pptx differ diff --git a/Documents_ref/Metodología.pdf b/Documents_ref/Metodología.pdf new file mode 100644 index 0000000..305d355 Binary files /dev/null and b/Documents_ref/Metodología.pdf differ diff --git a/Documents_ref/Metodología.pptx b/Documents_ref/Metodología.pptx new file mode 100644 index 0000000..13c2053 Binary files /dev/null and b/Documents_ref/Metodología.pptx differ diff --git a/Documents_ref/Oldy.pptx b/Documents_ref/Oldy.pptx new file mode 100644 index 0000000..269fee3 Binary files /dev/null and b/Documents_ref/Oldy.pptx differ diff --git a/Documents_ref/conf.sql b/Documents_ref/conf.sql new file mode 100644 index 0000000..12d50ad --- /dev/null +++ b/Documents_ref/conf.sql @@ -0,0 +1,42 @@ +-- export forma_db='{ +-- "host":"127.0.0.1", +-- "port":5432, +-- "database":"forma", +-- "user":"postgres", +-- "password":"Shala55951254" +-- }'; + +-- psql -h 127.0.0.1 -U postgres -a -f conf.sql + +DROP DATABASE IF EXISTS forma; + +CREATE DATABASE forma; + +\c forma; + +CREATE TABLE contact ( + id SERIAL PRIMARY KEY, + fecha VARCHAR(10), + hora VARCHAR(10), + nombre VARCHAR(50), + apellido VARCHAR(100), + email VARCHAR(150), + estado VARCHAR(30), + num_tel VARCHAR(20), + size_co VARCHAR(40), + rol_contacto VARCHAR(50), + industry_type VARCHAR(40), + tipo_req VARCHAR(255) +); + +CREATE TABLE users( + id VARCHAR(25), + nombre VARCHAR(50), + apellido VARCHAR(100), + email VARCHAR(150), + pswd VARCHAR(100), + isAdmin boolean +); + +INSERT INTO users (id, nombre, apellido, email, pswd, isAdmin) VALUES +('4HlOjqJ6jLISxNQIbs2Hzz', 'David', 'Itehua Xalamihua', 'davidix1991@gmail.com', '$2b$12$dbJWK5mv89PszxPeXlql5Otd8vv7kz6M44JnKZcrwJdKoovayiqEm', true); \ No newline at end of file diff --git a/Documents_ref/mediaquerys.css b/Documents_ref/mediaquerys.css new file mode 100644 index 0000000..d5a27d8 --- /dev/null +++ b/Documents_ref/mediaquerys.css @@ -0,0 +1,24 @@ +/* Smartphones (hasta 767px) */ +@media (max-width: 767px) { + /* body{ background-color: black; } */ +} + +/* Tablets (768px - 1023px) */ +@media (min-width: 768px) and (max-width: 1023px) { + /* body{ background-color: pink; } */ +} + +/* Laptops (1024px - 1439px) */ +@media (min-width: 1024px) and (max-width: 1439px) { + /* body{ background-color: purple; } */ +} + +/* PCs de escritorio (1440px - 1919px) */ +@media (min-width: 1440px) and (max-width: 1919px) { + /* body{ background-color: greenyellow; } */ +} + +/* Pantallas Ultrawide (1920px en adelante) */ +@media (min-width: 1920px) { + /* body{ background-color: red; } */ +} \ No newline at end of file diff --git a/Documents_ref/~$FORMHa Web_V2.pptx b/Documents_ref/~$FORMHa Web_V2.pptx deleted file mode 100644 index 0817a8b..0000000 Binary files a/Documents_ref/~$FORMHa Web_V2.pptx and /dev/null differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/forms_py/__pycache__/cls_db.cpython-312.pyc b/forms_py/__pycache__/cls_db.cpython-312.pyc new file mode 100644 index 0000000..39ae31e Binary files /dev/null and b/forms_py/__pycache__/cls_db.cpython-312.pyc differ diff --git a/forms_py/__pycache__/cls_db_usr.cpython-312.pyc b/forms_py/__pycache__/cls_db_usr.cpython-312.pyc new file mode 100644 index 0000000..b032c1f Binary files /dev/null and b/forms_py/__pycache__/cls_db_usr.cpython-312.pyc differ diff --git a/forms_py/__pycache__/cls_form_contact.cpython-312.pyc b/forms_py/__pycache__/cls_form_contact.cpython-312.pyc new file mode 100644 index 0000000..16dd2ed Binary files /dev/null and b/forms_py/__pycache__/cls_form_contact.cpython-312.pyc differ diff --git a/forms_py/__pycache__/cls_form_login.cpython-312.pyc b/forms_py/__pycache__/cls_form_login.cpython-312.pyc new file mode 100644 index 0000000..5c5c9b7 Binary files /dev/null and b/forms_py/__pycache__/cls_form_login.cpython-312.pyc differ diff --git a/forms_py/__pycache__/cls_recover_pswd.cpython-312.pyc b/forms_py/__pycache__/cls_recover_pswd.cpython-312.pyc new file mode 100644 index 0000000..e006f98 Binary files /dev/null and b/forms_py/__pycache__/cls_recover_pswd.cpython-312.pyc differ diff --git a/forms_py/__pycache__/functions.cpython-312.pyc b/forms_py/__pycache__/functions.cpython-312.pyc new file mode 100644 index 0000000..6c0e02b Binary files /dev/null and b/forms_py/__pycache__/functions.cpython-312.pyc differ diff --git a/forms_py/cls_db.py b/forms_py/cls_db.py new file mode 100644 index 0000000..f4cf1c0 --- /dev/null +++ b/forms_py/cls_db.py @@ -0,0 +1,44 @@ +import psycopg2 +from psycopg2 import sql +from psycopg2.extras import execute_values + +class DBContact: + def __init__(self, db_obj: dict): + """ + Inicializa la conexión a la base de datos. + + :param db_obj: Diccionario con las credenciales de la base de datos. + """ + self.db_obj = db_obj + + def _get_connection(self): + """ + Crea y retorna una nueva conexión a la base de datos. + """ + return psycopg2.connect( + host=self.db_obj['host'], + port=self.db_obj['port'], + database=self.db_obj['database'], + user=self.db_obj['user'], + password=self.db_obj['password'] + ) + + def carga_contact(self, data_tuple: tuple): + """ + Inserta un nuevo contacto en la base de datos. + + :param data_tuple: Tupla con los valores (nombre, apellido, email, num_tel, size_co, rol_contacto, industry_type, tipo_req). + :raises Exception: Si ocurre un error durante la ejecución de la consulta. + """ + query = sql.SQL(""" + INSERT INTO contact (fecha, hora, nombre, apellido, email, num_tel, size_co, rol_contacto, industry_type, tipo_req) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """) + + try: + with self._get_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(query, data_tuple) + conn.commit() + except Exception as e: + raise RuntimeError(f"Error al insertar contacto: {e}") diff --git a/forms_py/cls_db_usr.py b/forms_py/cls_db_usr.py new file mode 100644 index 0000000..bd5feeb --- /dev/null +++ b/forms_py/cls_db_usr.py @@ -0,0 +1,95 @@ +import psycopg2 +from psycopg2 import sql +from psycopg2.extras import execute_values + +class DBForma: + def __init__(self, db_obj: dict): + """ + Inicializa la conexión a la base de datos. + + :param db_obj: Diccionario con las credenciales de la base de datos. + Debe contener: host, port, database, user, password + """ + self.db_obj = db_obj + + def _get_connection(self): + """ + Crea y retorna una nueva conexión a la base de datos. + """ + return psycopg2.connect( + host=self.db_obj['host'], + port=self.db_obj['port'], + database=self.db_obj['database'], + user=self.db_obj['user'], + password=self.db_obj['password'] + ) + + def login(self, email: str): + """ + Verifica las credenciales de un usuario. + + :param email: Email del usuario a verificar + :return: Tupla con la contraseña si el usuario existe, None si no existe + :raises: RuntimeError si hay algún error en la consulta + """ + # Corrección 1: Usar parámetros correctamente para evitar SQL injection + query = "SELECT pswd FROM users WHERE email = %s;" + + try: + with self._get_connection() as conn: + with conn.cursor() as cursor: + # Corrección 2: Pasar parámetros como tupla (aunque sea uno solo) + cursor.execute(query, (email,)) + return cursor.fetchone() + except Exception as e: + raise RuntimeError(f"Error al verificar credenciales: {e}") + + def reset_pswd(self, email: str): + query = "SELECT email FROM users WHERE email = %s;" + try: + with self._get_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(query, (email,)) + return cursor.fetchone() + except Exception as e: + raise RuntimeError(f"Error al verificar credenciales: {e}") + + + def get_id(self, email: str): + query = "SELECT id FROM users WHERE email = %s;" + try: + with self._get_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(query, (email,)) + return cursor.fetchone() + except Exception as e: + raise RuntimeError(f"Error al verificar credenciales: {e}") + + + def update_pswd(self, pswd: str, email: str) -> bool: + """ + Actualiza la contraseña de un usuario en la base de datos. + + Args: + pswd: Nueva contraseña (debería estar hasheada) + email: Email del usuario a actualizar + + Returns: + bool: True si la actualización fue exitosa, False si no se actualizó ningún registro + + Raises: + RuntimeError: Si ocurre un error durante la operación + """ + query = "UPDATE users SET pswd = %s WHERE email = %s;" + try: + with self._get_connection() as conn: + with conn.cursor() as cursor: + cursor.execute(query, (pswd, email)) + # Verificar si realmente se actualizó algún registro + if cursor.rowcount == 0: + return False + conn.commit() # Confirmar explícitamente la transacción + return True + except Exception as e: + conn.rollback() # Revertir en caso de error + raise RuntimeError(f"Error al actualizar la contraseña: {e}") \ No newline at end of file diff --git a/forms_py/cls_form_contact.py b/forms_py/cls_form_contact.py new file mode 100644 index 0000000..78714be --- /dev/null +++ b/forms_py/cls_form_contact.py @@ -0,0 +1,124 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, TextAreaField, SelectField +from wtforms.validators import InputRequired, Email, Length, Optional, DataRequired + +class ContactForm(FlaskForm): + nombre = StringField( + "Nombre", validators=[InputRequired(message="Este campo es obligatorio"), Length(max=100, message="Máximo 100 caracteres")] + ) + + apellido = StringField( + "Apellido", validators=[InputRequired(message="Este campo es obligatorio"), Length(max=100, message="Máximo 100 caracteres")] + ) + + email = StringField( + "Email", validators=[InputRequired(message="Este campo es obligatorio"), Email(message="Ingresa un email válido"), Length(max=120)] + ) + + estado = SelectField("Estado", choices=[ + ("", ""), + (1, 'Aguascalientes'), + (2, 'Baja California'), + (3, 'Baja California Sur'), + (4, 'Campeche'), + (5, 'Chiapas'), + (6, 'Chihuahua'), + (7, 'Ciudad de México'), + (8, 'Coahuila'), + (9, 'Colima'), + (10, 'Durango'), + (11, 'Estado de México'), + (12, 'Guanajuato'), + (13, 'Guerrero'), + (14, 'Hidalgo'), + (15, 'Jalisco'), + (16, 'Michoacán'), + (17, 'Morelos'), + (18, 'Nayarit'), + (19, 'Nuevo León'), + (20, 'Oaxaca'), + (21, 'Puebla'), + (22, 'Querétaro'), + (23, 'Quintana Roo'), + (24, 'San Luis Potosí'), + (25, 'Sinaloa'), + (26, 'Sonora'), + (27, 'Tabasco'), + (28, 'Tamaulipas'), + (29, 'Tlaxcala'), + (30, 'Veracruz'), + (31, 'Yucatán'), + (32, 'Zacatecas') + ], validators=[DataRequired(message="Debes seleccionar un estado.")],) + + + num_tel = StringField( + "Número telefónico", validators=[InputRequired(message="Número requerido"), Length(min=8, max=20, message="Número inválido")] + ) + + size_co = SelectField( "Tamaño empresa", choices=[ + ("", ""), + ("1-10", "1 - 20"), + ("10-50", "21 - 50"), + ("50-100", "51 - 100"), + ("101-149", "101 - 149"), + ("150-500", "150 - 500"), + ("501-799", "501 - 799"), + ("800-5,000", "800 - 5,000"), + ("5,000-10,000", "5,001 - 10,000"), + ("10,000+", "10,000+") + ], validators=[DataRequired(message="Debes seleccionar el tamaño de la empresa.")],) + + + rol_contacto = SelectField( "Rol desempeñado", choices=[ + ("", ""), + (1, 'Capacitaciones'), + (2, 'Gerente de Operaciones'), + (3, 'Gerente de TI'), + (4, 'Nómina'), + (5, 'Desarrollo Organizacional'), + (6, 'Reclutamiento y selección'), + (7, 'Gerencia de Recursos Humanos'), + (8, 'Finanzas / Gerencia'), + (9, 'Dueño de mi propio negocio'), + (10, 'Estudiante'), + (11, 'Otro') + ], validators=[DataRequired(message="Debes selecionar tu rol.")],) + + industry_type = SelectField( "Sector", choices=[ + ("", ""), + (1, 'Agricola'), + (2, 'Alimentos'), + (3, 'Automotriz'), + (4, 'Comercio'), + (5, 'Comunicaciones'), + (6, 'Construcción'), + (7, 'Consultora'), + (8, 'Educación'), + (9, 'Empresas B'), + (10, 'Energia'), + (11, 'Entretenimiento'), + (12, 'Financiera'), + (13, 'Fundación'), + (14, 'Holding'), + (15, 'Hoteleria'), + (16, 'Legal'), + (17, 'Logistica'), + (18, 'Manufacturera'), + (19, 'Marketing'), + (20, 'Minera'), + (21, 'Otra'), + (22, 'Sector Público'), + (23, 'Restoranes/Cafeteria'), + (24, 'RRHH'), + (25, 'Salud'), + (26, 'Seguridad'), + (27, 'Servicios'), + (28, 'Tecnología'), + (29, 'Transporte'), + (30, 'Utilities (gas, agua, electricidad)') + ], validators=[DataRequired(message="Selecione una propiedad.")],) + + tipo_req = TextAreaField( + "Objetivo del contacto", validators=[InputRequired(message="Describe brevemente tu necesidad"), Length(min=10, max=500, message="Mínimo 10 caracteres, máximo 500")] + ) \ No newline at end of file diff --git a/forms_py/cls_form_login.py b/forms_py/cls_form_login.py new file mode 100644 index 0000000..d0ca9a0 --- /dev/null +++ b/forms_py/cls_form_login.py @@ -0,0 +1,35 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField +from wtforms.validators import InputRequired, Email, Length, DataRequired + +class LogIn(FlaskForm): + """ + Formulario de inicio de sesión que hereda de FlaskForm. + + Campos: + email (StringField): Campo para el correo electrónico del usuario. + password (PasswordField): Campo para la contraseña del usuario. + + Validaciones: + - Email: Requerido, formato válido y longitud máxima de 120 caracteres. + - Password: Requerido. + """ + + email = StringField( + "Email", + validators=[ + InputRequired(message="Este campo es obligatorio"), + Email(message="Ingresa un email válido"), + Length(max=120, message="El email no puede exceder los 120 caracteres") + ], + description="Correo electrónico registrado en la plataforma" + ) + + password = PasswordField( + 'Contraseña', + validators=[ + DataRequired(message="La contraseña es obligatoria") + ], + description="Contraseña de acceso a la cuenta", + render_kw={"placeholder": "Ingresa tu contraseña"} + ) \ No newline at end of file diff --git a/forms_py/cls_recover_pswd.py b/forms_py/cls_recover_pswd.py new file mode 100644 index 0000000..e6d57fe --- /dev/null +++ b/forms_py/cls_recover_pswd.py @@ -0,0 +1,33 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, validators +from wtforms.validators import DataRequired, Email, Length + +class RecoverPswd(FlaskForm): + """ + Formulario para recuperación de contraseña. + + Campos: + email (StringField): Campo para el correo electrónico del usuario con validaciones. + + Validaciones: + - Requerido (DataRequired) + - Formato válido de email (Email) + - Longitud máxima de 120 caracteres (Length) + - Sanitización automática (elimina espacios) + """ + + email = StringField( + label="Correo Electrónico", + description="Ingresa el email asociado a tu cuenta", + filters=[lambda x: x.strip() if x else x], # Sanitización + validators=[ + DataRequired(message="El correo electrónico es requerido"), + Email(message="Por favor ingresa un email válido"), + Length(max=120, message="El email no puede exceder los 120 caracteres") + ], + render_kw={ + "placeholder": "ejemplo@correo.com", + "class": "form-control", # Para Bootstrap + "autocomplete": "email" + } + ) \ No newline at end of file diff --git a/forms_py/functions.py b/forms_py/functions.py new file mode 100644 index 0000000..f5d44fc --- /dev/null +++ b/forms_py/functions.py @@ -0,0 +1,96 @@ +from email.message import EmailMessage +import smtplib +import os +import json +import string +import secrets +from flask_bcrypt import Bcrypt +import shortuuid + + +bcrypt = Bcrypt() + + +v = { + 'home': 'home/home.html', + 'about-us': 'about-us/about-us.html', + 'solutions': 'solutions/solutions.html', + 'methodology': 'methodology/methodology.html', + 'contact': 'contact/contact.html', + 'login': 'login/login.html', + 'usr_home': 'usr_home/usr_home.html', + 'recover_pswd': 'login/recover_pswd.html' +} + + +def db_conf_obj(dbEnvVarName: str) -> object: + ''' + dbEnvVarName = nombre de la variable de entorno + variable que contiene los datos de la base de datos + ''' + db = os.getenv(dbEnvVarName) + db = json.loads(db) + return db + +def generar_contrasena(): + """ + Genera una contraseña aleatoria de 25 caracteres. + + La contraseña se compone de una combinación de letras mayúsculas, letras minúsculas y dígitos. La función utiliza + el módulo `secrets` para asegurar que la generación de la contraseña sea adecuada para propósitos de seguridad. + + Returns: + ------- + str + Una cadena de caracteres aleatorios de 25 caracteres, formada por letras (mayúsculas y minúsculas) y dígitos. + + Example: + -------- + >>> generar_contrasena() + 'A9gTz5bNp0Wk3L6Xq2vUv8YwRz1E' + """ + caracteres = string.ascii_letters + string.digits + contrasena = ''.join(secrets.choice(caracteres) for _ in range(25)) + return contrasena + +def hash_password(password): + """ + Genera un hash de la contraseña utilizando bcrypt. + + Esta función toma una contraseña en texto plano y la convierte en un hash seguro utilizando el algoritmo bcrypt. + El hash generado es una cadena en formato UTF-8, adecuada para almacenamiento en bases de datos. + + Parámetros: + ---------- + password : str + La contraseña en texto plano que se desea hashear. + + Returns: + ------- + str or None + El hash de la contraseña en formato UTF-8 si la operación es exitosa. + Devuelve `None` en caso de que ocurra una excepción durante el proceso de hashing. + + Example: + -------- + >>> hash_password('mi_contrasena_segura') + '$2b$12$D4yU/jEaK4xdgK0R2J6c6Odnk8p3k/RtG2ByjF26.4gnBR4tdA/2i' + """ + try: + hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') + return hashed_password + except Exception as e: + return None + +def getRandomId(): + # POR DEFAULT SHORUUID SACA 22 CARACTERES + # LOS PODEMOS ACORTAR DE LA SIGUIENTE MANERA: short_id_custom = shortuuid.uuid()[:10] + # Configurar el generador con una semilla específica (opcional) + # shortuuid.set_alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-!#$!%&/()=?¡¿") + shortuuid.set_alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + # Generar un UUID corto con la semilla personalizada + short_id_custom = shortuuid.uuid() + return short_id_custom + + + diff --git a/main.py b/main.py index e69de29..364ab7d 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,230 @@ +from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt +from flask_mail import Message, Mail +from threading import Thread +from flask import Flask, render_template, redirect, url_for, flash, current_app # Añadí current_app +from flask import jsonify, make_response # Añade estas importaciones +from forms_py.cls_form_contact import ContactForm +from forms_py.cls_form_login import LogIn +from forms_py.cls_db import DBContact +from forms_py.cls_db_usr import DBForma +from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v +from forms_py.cls_recover_pswd import RecoverPswd +import os +from datetime import datetime, timedelta +from flask_bcrypt import Bcrypt + +app = Flask(__name__) +bcrypt = Bcrypt(app) + + +email_sender = os.getenv("email_sender") +email_pswd = os.getenv("pswd_formha") +lst_email_to = ["davidix1991@gmail.com", "davicho1991@live.com"] + +# Configuración de JWT (añade esto junto a tus otras configs) +app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'super-secret-fallback-key') # Usa una clave segura en producción -> MOVER A VARIABLE DE ENTORNO +# app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) # Token expira en 1 hora +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=30) # Token expira en 1 hora +app.config['JWT_COOKIE_SECURE'] = True # En producción debe ser True +app.config['JWT_COOKIE_CSRF_PROTECT'] = True # Recomendado para seguridad +app.config['JWT_TOKEN_LOCATION'] = ['cookies'] +# FLASK EMAIL +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 465 +app.config['MAIL_USE_SSL'] = True +app.config['MAIL_USERNAME'] = email_sender # email en variable de entorno +app.config['MAIL_PASSWORD'] = email_pswd # contraseña en variable de entorno +app.config['SECRET_KEY'] = 'FoRmHä$2025' # Necesario para CSRF y mensajes flash -> la debo colocar en variable de entono? + + +mail = Mail(app) + +jwt = JWTManager(app) + +jsonDbContact = db_conf_obj("forma_db") +dbContact = DBContact(jsonDbContact) +dbUsers = DBForma(jsonDbContact) + + + +def send_async_email(app, msg): + with app.app_context(): + mail.send(msg) + +@app.route('/') +def home(): + return render_template(v['home'], active_page='home') + +@app.route('/about-us') +def about_us(): + return render_template(v['about-us'], active_page='about_us') + +@app.route('/solutions') +def solutions(): + return render_template(v['solutions'], active_page='solutions') + +@app.route('/methodology') +def methodology(): + return render_template(v['methodology'], active_page='methodology') + +@app.route("/contact", methods=['GET', 'POST']) +def contact(): + form = ContactForm() + + if form.validate_on_submit(): # Corregí "validate_on_sumbit" a "validate_on_submit" + cur_date = datetime.now().strftime("%d/%m/%Y") + cur_hour = datetime.now().strftime("%H:%M") + + # Procesar datos del formulario + flash('¡Gracias por contactarnos! Te responderemos pronto.', 'success') + data = (cur_date, cur_hour, form.nombre.data, form.apellido.data, + form.email.data, form.num_tel.data, form.size_co.data, + form.rol_contacto.data, form.industry_type.data, form.tipo_req.data) + dbContact.carga_contact(data) + + # Configurar y enviar email asíncrono + msg = Message( + "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to + ) + + msg.html = """ +

Nuevo contacto recibido

+

Nombre: {} {}

+

Email: {}

+

Teléfono: {}

+

Mensaje: {}

+ """.format(form.nombre.data, form.apellido.data, form.email.data, + form.num_tel.data, form.tipo_req.data) + + # Enviar en segundo plano + thr = Thread( + target=send_async_email, + args=(current_app._get_current_object(), msg) + ) + thr.start() + + return redirect(url_for('contact')) + + return render_template(v['contact'], form=form, active_page='contact') + + +@app.route("/login", methods=['GET', 'POST']) +def login(): + form = LogIn() + if form.validate_on_submit(): + f_email = f"{form.email.data}".lower() + f_pswd = form.password.data + res_pswd_server = dbUsers.login((f_email)) + + if res_pswd_server is None: + flash('Usuario no registrado en la db', 'error') + return redirect(url_for('login')) + + res_pswd_server = res_pswd_server[0] + if bcrypt.check_password_hash(res_pswd_server, f_pswd): + + id_user = dbUsers.get_id(f_email)[0] + + # Crear token JWT + access_token = create_access_token(identity=id_user) + + # Redirigir a usr_home + response = make_response(redirect(url_for('usr_home'))) + response.set_cookie('access_token_cookie', access_token, httponly=True, secure=True, samesite='Lax' ) + flash('Inicio de sesión exitoso', 'success') + return response + else: + flash('Credenciales incorrectas', 'error') + + return render_template(v['login'], form=form, active_page='login') + + +@app.route("/recover-pswd", methods=['GET', 'POST']) +def recover_pswd(): + form = RecoverPswd() + if form.validate_on_submit(): + f_email = f"{form.email.data}".lower() + emailPswdReco = dbUsers.reset_pswd(f_email) + + if emailPswdReco is None: + flash('Email no válido', 'error') + return render_template(v['recover_pswd'], form=form, active_page='login') + + emailPswdReco = emailPswdReco[0] + new_tmp_pswd = generar_contrasena() + hashed_new_pswd = hash_password(new_tmp_pswd) + + # Configurar y enviar email asíncrono + msg = Message( + "Subject: Recuperación de contraseña", + sender=email_sender, + recipients=[emailPswdReco] # Asegúrate que es una lista + ) + + # msg.html = render_template( 'email/recover_pswd.html', temp_password=new_tmp_pswd ) + + msg.html = """ +

Nueva contraseña temporal:

+

Contraseña: {}

+

Una vez iniciada tu sesión debes de cambiar la contraseña a una nueva que puedas recordar

+ """.format(new_tmp_pswd) + + dbUsers.update_pswd(pswd=hashed_new_pswd, email=emailPswdReco) + + # Enviar en segundo plano + thr = Thread( + target=send_async_email, + args=(current_app._get_current_object(), msg) + ) + thr.start() + + flash("Se ha enviado una contraseña temporal a tu correo electrónico", "success") + return redirect(url_for('login')) # Redirige en lugar de renderizar + + return render_template(v['recover_pswd'], form=form, active_page='login') + + + +@app.route('/user/home') +@jwt_required() # Protege esta ruta +def usr_home(): + current_user = get_jwt_identity() # Obtiene el identity (normalmente el email) + token_data = get_jwt() # Obtiene TODOS los datos del token decodificado + + # print("Token completo:", token_data) + # print("Usuario:", current_user) + + + return render_template(v['usr_home'], current_user=current_user, token_data=token_data) + + +# ------------------------------------------------------------- + +# Manejo de errores JWT +@jwt.expired_token_loader +def handle_expired_token(jwt_header, jwt_payload): + flash('Tu sesión ha expirado. Por favor inicia sesión nuevamente', 'warning') + return redirect(url_for('login')) + +@jwt.unauthorized_loader +def handle_unauthorized_error(reason): + flash(f'Debes iniciar sesión para acceder a esta página: {reason}', 'error') + return redirect(url_for('login')) + +@jwt.invalid_token_loader +def handle_invalid_token_error(reason): + flash(f'Sesión inválida: {reason}', 'error') + return redirect(url_for('login')) + +@app.route("/logout") +def logout(): + response = make_response(redirect(url_for('login'))) + response.delete_cookie('access_token_cookie') + flash('Sesión cerrada correctamente', 'success') + return response + +# ------------------------------------------------------------- + + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0', port=8089) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..65e28ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +bcrypt==4.3.0 +blinker==1.9.0 +click==8.1.8 +dnspython==2.7.0 +email_validator==2.2.0 +Flask==3.1.0 +Flask-Bcrypt==1.0.1 +Flask-JWT-Extended==4.7.1 +Flask-Mail==0.10.0 +Flask-WTF==1.2.2 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +psycopg2-binary==2.9.10 +PyJWT==2.10.1 +python-dotenv==1.1.0 +shortuuid==1.0.13 +Werkzeug==3.1.3 +WTForms==3.2.1 diff --git a/static/about-us/about-us.css b/static/about-us/about-us.css new file mode 100644 index 0000000..ee08bec --- /dev/null +++ b/static/about-us/about-us.css @@ -0,0 +1,210 @@ +/** + * EXPANDING PANELS - Componente de paneles expandibles + * + * Estilos para un sistema de paneles con: + * - Efecto hover con animación + * - Texto expandible con scroll + * - Diseño responsive + */ + + .expanding-panels { + display: grid; + gap: 1em; + + /* ---------------------------- */ + /* ESTILOS BASE DEL PANEL */ + /* ---------------------------- */ + .panel { + position: relative; + background-size: cover; + background-position: center; + border-radius: 10px; + overflow: hidden; + transition: all 0.5s ease; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + + /* ---------------------------- */ + /* CONTENIDO DEL PANEL */ + /* ---------------------------- */ + .panel-content { + text-align: center; + color: #000; + width: 90%; + } + + /* Título del panel - siempre visible */ + .panel-title { + background-color: rgba(255, 255, 255, 0.85); + padding: 0.5em 1em; + border-radius: 10px; + transition: all 0.5s ease; + + h3 { + margin: 0; + } + } + + /* Texto expandible - con scroll vertical cuando es necesario */ + .panel-text { + max-height: 0; + opacity: 0; + overflow-y: hidden; /* Oculta scroll inicialmente */ + transition: all 0.5s ease; + margin-top: 1em; + background-color: rgba(255, 255, 255, 0.85); + padding: 0 1em; + border-radius: 10px; + + & p, ul { + text-align: justify; + text-justify: inter-word; + } + } + + /* ---------------------------- */ + /* EFECTOS HOVER */ + /* ---------------------------- */ + .panel:hover { + transform: scale(1.05); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); + z-index: 2; + + .panel-text { + max-height: 50vh; /* Altura máxima antes de mostrar scroll */ + opacity: 1; + padding: 1em; + overflow-y: auto; /* Muestra scroll solo cuando es necesario */ + + /* Estilos personalizados para la barra de scroll */ + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.2); + border-radius: 3px; + } + } + } + + /* ---------------------------- */ + /* IMÁGENES DE FONDO */ + /* ---------------------------- */ + .p1 { background-image: url('/static/img/about-us/team.avif'); } + .p2 { background-image: url('/static/img/about-us/mision.avif'); } + .p3 { background-image: url('/static/img/about-us/idea.avif'); } + .p4 { background-image: url('/static/img/about-us/mex.avif'); } +} + +/* ============================================ */ +/* MEDIA QUERIES - RESPONSIVE DESIGN */ +/* ============================================ */ + +/* Smartphones (hasta 767px) */ +@media (max-width: 767px) { + + main{ + width: 95vw; + margin: 1em auto; + min-height: 100vh; + margin-top: 1em; + margin-bottom: 1em; + } + + .expanding-panels { + grid-template-rows: repeat(4, 1fr); + row-gap: 2em; + + .panel { + height: 45vh; + } + } +} + +/* Tablets (768px - 1023px) */ +@media (min-width: 768px) and (max-width: 1023px) { + + main{ + width: 90vw; + margin: auto; + min-height: 85vh; + margin-top: 1em; + margin-bottom: 1em; + } + + .expanding-panels { + grid-template-columns: repeat(2, 1fr); + + .panel { + height: 45vh; + } + } +} + +/* Laptops (1024px - 1439px) */ +@media (min-width: 1024px) and (max-width: 1439px) { + + main{ + width: 90vw; + margin: auto; + min-height: 85vh; + margin-top: 1em; + margin-bottom: 1em; + } + + .expanding-panels { + grid-template-columns: repeat(2, 1fr); + + .panel { + height: 40vh; + } + } +} + +/* PCs de escritorio (1440px - 1919px) */ +@media (min-width: 1440px) and (max-width: 1919px) { + + .expanding-panels { + grid-template-columns: repeat(4, 1fr); + + .panel { + height: 75vh; + } + } + + main{ + width: 90vw; + margin: auto; + min-height: 85vh; + margin-top: 1em; + margin-bottom: 1em; + } + + +} + +/* Pantallas Ultrawide (1920px en adelante) */ +@media (min-width: 1920px) { + + .expanding-panels { + grid-template-columns: repeat(4, 1fr); + + .panel { + height: 75vh; + } + } + + main{ + width: 80vw; + margin: auto; + min-height: 85vh; + margin-top: 1em; + margin-bottom: 1em; + } + + + +} \ No newline at end of file diff --git a/static/home/home.css b/static/home/home.css new file mode 100644 index 0000000..117c60b --- /dev/null +++ b/static/home/home.css @@ -0,0 +1,140 @@ +/* Variables para consistencia */ +:root { + --primary-color: #00acc1; + --shadow-light: 0 3px 4px rgba(0, 0, 0, 0.4); + --shadow-hover: 0 15px 26px rgba(0, 0, 0, 0.5); + --card-radius: 5px; + --image-height: 250px; + --banner-text-color: #ffff; + --banner-overlay: rgba(116, 118, 119, 0.8); +} + +/* Estructura principal */ +/* main { + width: 95vw; + min-height: 100vh; +} */ + +/* ---------------------------- */ +/* Fila 1 - Banner con efecto de zoom */ +.r-i { + width: 100%; +} + +.r-i-ii { + background-image: url('/static/img/home/reunion.avif'); + background-size: 100%; + background-repeat: no-repeat; + background-position: center; + border-radius: 10px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + transition: background-size 2s ease-in-out; + height: 35vh; /* Valor por defecto para móviles */ +} + +.r-i-ii:hover { + background-size: 110%; /* Efecto de zoom mantenido */ +} + +.r-i-ii spam { + background-color: var(--banner-overlay); + padding: 20px; + border-radius: 10px; + text-align: center; + font-size: clamp(1rem, 3vw, 25px); /* Texto responsivo */ + color: var(--banner-text-color); + max-width: 80%; +} + +/* ---------------------------- */ +/* Fila 2 - Tarjetas con efectos */ +.r-ii { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; + margin-top: 1em; +} + +.card-flyer { + min-height: 100%; + background: #fff; + border-radius: var(--card-radius); + box-shadow: var(--shadow-light); + transition: transform 0.3s, box-shadow 0.3s; + overflow: hidden; +} + +.image-box img { + width: 100%; + height: var(--image-height); + object-fit: cover; + transition: transform 0.9s; +} + +.text-container { + padding: 20px; + text-align: center; +} + +.text-container h6 { + margin: 0; + font-size: 18px; + color: var(--primary-color); +} + +/* Efectos hover (completos) */ +.card-flyer:hover { + transform: translateY(-10px); + box-shadow: var(--shadow-hover); +} + +.card-flyer:hover img { + transform: scale(1.15); /* Efecto de escala mantenido */ +} + +/* ---------------------------- */ +/* Media Queries optimizadas */ +@media (min-width: 768px) { + main { + width: 90vw; + } + .r-i-ii { + height: 35vh; + } +} + +@media (min-width: 1024px) { + main { + width: 85vw; + margin: 20px auto; + } + .r-i-ii { + height: 45vh; + } +} + +@media (min-width: 1440px) { + main { + width: 80vw; + margin: 2em auto; + } + .r-i-ii { + height: 50vh; + } +} + +@media (min-width: 1920px) { + .r-i-ii { + height: 60vh; + } +} + +/* Ajuste específico para texto en móviles */ +@media (max-width: 767px) { + .r-i-ii spam { + font-size: 1em; + } +} \ No newline at end of file diff --git a/static/img/about-us/idea.avif b/static/img/about-us/idea.avif new file mode 100644 index 0000000..237978a Binary files /dev/null and b/static/img/about-us/idea.avif differ diff --git a/static/img/about-us/mex.avif b/static/img/about-us/mex.avif new file mode 100644 index 0000000..70f062c Binary files /dev/null and b/static/img/about-us/mex.avif differ diff --git a/static/img/about-us/mision.avif b/static/img/about-us/mision.avif new file mode 100644 index 0000000..719e546 Binary files /dev/null and b/static/img/about-us/mision.avif differ diff --git a/static/img/about-us/team.avif b/static/img/about-us/team.avif new file mode 100644 index 0000000..429787a Binary files /dev/null and b/static/img/about-us/team.avif differ diff --git a/static/img/home/chair.avif b/static/img/home/chair.avif new file mode 100644 index 0000000..a21d0ef Binary files /dev/null and b/static/img/home/chair.avif differ diff --git a/static/img/home/reunion.avif b/static/img/home/reunion.avif new file mode 100644 index 0000000..0163e5a Binary files /dev/null and b/static/img/home/reunion.avif differ diff --git a/static/img/home/software.avif b/static/img/home/software.avif new file mode 100644 index 0000000..72809e8 Binary files /dev/null and b/static/img/home/software.avif differ diff --git a/static/img/home/work.avif b/static/img/home/work.avif new file mode 100644 index 0000000..46e7eab Binary files /dev/null and b/static/img/home/work.avif differ diff --git a/static/img/solutions/Imagen1.png b/static/img/solutions/Imagen1.png new file mode 100644 index 0000000..0d458a3 Binary files /dev/null and b/static/img/solutions/Imagen1.png differ diff --git a/static/img/solutions/Imagen2.png b/static/img/solutions/Imagen2.png new file mode 100644 index 0000000..6e5015b Binary files /dev/null and b/static/img/solutions/Imagen2.png differ diff --git a/static/img/solutions/Imagen3.png b/static/img/solutions/Imagen3.png new file mode 100644 index 0000000..8bed2ac Binary files /dev/null and b/static/img/solutions/Imagen3.png differ diff --git a/static/img/solutions/Imagen4.png b/static/img/solutions/Imagen4.png new file mode 100644 index 0000000..b97a517 Binary files /dev/null and b/static/img/solutions/Imagen4.png differ diff --git a/static/img/solutions/Imagen5.png b/static/img/solutions/Imagen5.png new file mode 100644 index 0000000..0881c69 Binary files /dev/null and b/static/img/solutions/Imagen5.png differ diff --git a/static/img/solutions/Imagen6.png b/static/img/solutions/Imagen6.png new file mode 100644 index 0000000..3854e53 Binary files /dev/null and b/static/img/solutions/Imagen6.png differ diff --git a/static/methodology/methodology.css b/static/methodology/methodology.css new file mode 100644 index 0000000..e69de29 diff --git a/static/solutions/solutions.css b/static/solutions/solutions.css new file mode 100644 index 0000000..245aac2 --- /dev/null +++ b/static/solutions/solutions.css @@ -0,0 +1,185 @@ +main { + background-image: url('/static/img/solutions/Imagen1.png'); + background-repeat: no-repeat; + background-position: center bottom; + background-color: #50164A; +} + + +.parent { + padding: 5em; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 2em; + /* width: 75vw; */ + /* Altura para que la grilla sea visible */ + /* height: 80vh; */ + & div { + background-size: contain; + background-position: center; + background-repeat: no-repeat; + } +} + + +.parent div { + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + border: 2px solid transparent; + + &:hover { + animation: vibrate 0.4s ease infinite; + transform: scale(1.03); + box-shadow: 0 5px 15px rgba(255, 255, 255, 0.4); + border-color: #00acc1; + filter: brightness(1.05); + z-index: 10; + border-radius: 15px; + } +} + +/* Imágenes específicas */ +.i-i { + background-image: url('/static/img/solutions/Imagen2.png'); +} + +.i-ii { + background-image: url('/static/img/solutions/Imagen3.png'); +} + +.i-iii { + background-image: url('/static/img/solutions/Imagen4.png'); +} + +.ii-i { + background-image: url('/static/img/solutions/Imagen5.png'); + grid-row: 2; +} + +.ii-ii { + background-image: url('/static/img/solutions/Imagen6.png'); + grid-column: 3; + grid-row: 2; +} + + +/* Smartphones (hasta 767px) */ +@media (max-width: 767px) { + + main { + width: 95vw; + min-height: 70vh; + margin: 10px auto; + background-size: auto 15%; + border-radius: 12px; + padding: 1em; /* Añadido para espacio interno */ + } + + .parent { + width: 100vw; /* Cambiado a 100% para mejor ajuste */ + padding: 1em; /* Reducido el padding para móviles */ + + display: grid; + grid-template-columns: 1fr; /* Solo una columna en móviles */ + grid-template-rows: repeat(6, minmax(120px, 1fr)); /* Altura mínima garantizada */ + gap: 1em; /* Espacio reducido para móviles */ + } + + /* Asegurar que todos los divs sean visibles */ + .parent div { + min-height: 120px; /* Altura mínima garantizada */ + width: 100% !important; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + border: 2px solid rgba(255, 255, 255, 0.5); /* Borde más visible */ + } + + /* Posicionamiento específico para móviles */ + .ii-i { + grid-row: 4; /* Posición ajustada */ + } + + .ii-ii { + grid-row: 5; /* Posición ajustada */ + grid-column: 1; /* Reset para móviles */ + } + +} + +/* Tablets (768px - 1023px) */ +@media (min-width: 768px) and (max-width: 1023px) { + + main { + width: 90vw; + min-height: 70vh; + margin: 10px auto; + background-size: auto 35%; + border-radius: 12px; + } + + .parent { + width: 90vw; + /* Altura para que la grilla sea visible */ + height: 80vh; + } + +} + +/* Laptops (1024px - 1439px) */ +@media (min-width: 1024px) and (max-width: 1439px) { + + main { + width: 85vw; + min-height: 85vh; + margin: 10px auto; + background-size: auto 50%; + border-radius: 12px; + } + + .parent { + width: 80vw; + /* Altura para que la grilla sea visible */ + height: 80vh; + } + +} + +/* PCs de escritorio (1440px - 1919px) */ +@media (min-width: 1440px) and (max-width: 1919px) { + + main { + width: 80vw; + min-height: 85vh; + margin: 10px auto; + background-size: auto 50%; + border-radius: 12px; + } + + .parent { + width: 70vw; + /* Altura para que la grilla sea visible */ + height: 70vh; + } + + +} + +/* Pantallas Ultrawide (1920px en adelante) */ +@media (min-width: 1920px) { + + main { + width: 80vw; + min-height: 85vh; + margin: 10px auto; + background-size: auto 50%; + border-radius: 12px; + } + + .parent { + width: 70vw; + /* Altura para que la grilla sea visible */ + height: 70vh; + } + +} \ No newline at end of file diff --git a/static/template/navbar.css b/static/template/navbar.css new file mode 100644 index 0000000..4f77b52 --- /dev/null +++ b/static/template/navbar.css @@ -0,0 +1,59 @@ + +.navbar-custom { + background-color: #50164A !important; + font-size: 20px; + /* min-height: 80px; */ + + & .navbar-brand, .nav-link { + color: white !important; + } + + & .navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 1%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + } + + & .navbar-nav .nav-item .nav-link { + position: relative; + text-decoration: none; + } + + /* Efecto de hover */ + & .navbar-nav.effect-3 .nav-item .nav-link:before, + & .navbar-nav.effect-3 .nav-item .nav-link:after { + content: ""; + height: 1px; + width: 0; + opacity: 0; + background-color: #fff; + position: absolute; + transition: all .4s; + } + + & .navbar-nav.effect-3 .nav-item .nav-link:before { + top: 0px; + } + + & .navbar-nav.effect-3 .nav-item .nav-link:after { + bottom: 0px; + right: 0; + } + + /* Aplicar el efecto de hover al enlace activo */ + & .navbar-nav.effect-3 .nav-item .nav-link.active:before, + & .navbar-nav.effect-3 .nav-item .nav-link.active:after { + width: calc(100% + 0px); + opacity: 0.9; + } + + /* Aplicar el efecto de hover al hacer hover */ + & .navbar-nav.effect-3 .nav-item .nav-link:hover:before, + & .navbar-nav.effect-3 .nav-item .nav-link:hover:after { + width: calc(100% + 0px); + opacity: 0.9; + } + + & li.nav-item { + margin-left: 1em; + margin-right: 1em; + } +} \ No newline at end of file diff --git a/static/template/tmp.css b/static/template/tmp.css new file mode 100644 index 0000000..12dd167 --- /dev/null +++ b/static/template/tmp.css @@ -0,0 +1,13 @@ +main { + display: grid; + place-content: center; +} +/* main { + width: 80%; + margin: auto; + text-align: center; + margin-top: 5em; + margin-bottom: 5em; + min-height: 85vh; + height: 85vh; +} */ diff --git a/templates/about-us/about-us.html b/templates/about-us/about-us.html new file mode 100644 index 0000000..13ef983 --- /dev/null +++ b/templates/about-us/about-us.html @@ -0,0 +1,78 @@ +{% extends 'template.html' %} + +{% block css %} + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + +
+ +
+
+
+

¿Quiénes Somos?

+
+
+

Somos Cecilia y Enrique, profesionistas en Capital Humano y Desarrollo Organizacional, con más de 20 años de experiencia impulsando el crecimiento de empresas a través de estrategias de talento 360º.

+

Fundamos {% include 'comps/forma.html' %} para transformar la gestión del talento con soluciones personalizadas, basadas en datos confiables y estrategias innovadoras. A través de alianzas estratégicas y nuestras propias iniciativas, ayudamos a las empresas a tomar decisiones informadas, optimizando su capital humano con herramientas efectivas y de alto impacto.

+

Con {% include 'comps/forma.html' %}, tu equipo no solo crece, sino que evoluciona.

+
+
+
+ + +
+
+
+

Misión

+
+
+

Impulsar el crecimiento y la competitividad de nuestra clientela a través de soluciones integrales en materia de capital humano. Diseñadas con la más alta calidad, precisión y puntualidad.

+

Destacarnos por brindar asesoría confiable y estratégica, integrando tecnología avanzada y las mejores prácticas del sector para optimizar la gestión del talento y el desarrollo organizacional.

+
+
+
+ + +
+
+
+

Visión

+
+
+

Ser la consultoría de referencia en recursos humanos, reconocida por nuestra innovación, precisión y compromiso con la excelencia.

+

Nos esforzamos por transformar la manera en que las empresas desarrollan su talento, haciendo que la profesionalización y el crecimiento sean accesibles. A través de soluciones tecnológicas y enfoques estratégicos e innovadores, impulsamos el éxito de nuestros clientes y la evolución del talento humano.

+
+
+
+ + +
+
+
+

Valores

+
+
+

+

    +
  • Excelencia: Nos involucramos con la calidad en cada servicio y producto que entregamos.
  • +
  • Innovación: Adoptamos tecnología de vanguardia para ofrecer soluciones eficientes y competitivas.
  • +
  • Responsabilidad: Construimos relaciones de confianza basadas en resultados.
  • +
  • Precisión: Brindamos diagnósticos, estrategias y resultados con el más alto grado de exactitud.
  • +
  • Ética: Actuamos con integridad y transparencia en cada interacción con nuestros clientes.
  • +
+

+
+
+
+
+ + + +{% endblock body %} \ No newline at end of file diff --git a/templates/comps/footer.html b/templates/comps/footer.html new file mode 100644 index 0000000..148462e --- /dev/null +++ b/templates/comps/footer.html @@ -0,0 +1,73 @@ + + + + \ No newline at end of file diff --git a/templates/comps/forma.html b/templates/comps/forma.html new file mode 100644 index 0000000..d22fc69 --- /dev/null +++ b/templates/comps/forma.html @@ -0,0 +1 @@ +FORMHä \ No newline at end of file diff --git a/templates/comps/navbar.html b/templates/comps/navbar.html new file mode 100644 index 0000000..c442811 --- /dev/null +++ b/templates/comps/navbar.html @@ -0,0 +1,57 @@ + + diff --git a/templates/comps/navbar_usr.html b/templates/comps/navbar_usr.html new file mode 100644 index 0000000..d8b6c2d --- /dev/null +++ b/templates/comps/navbar_usr.html @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/templates/contact/contact.html b/templates/contact/contact.html new file mode 100644 index 0000000..8c90174 --- /dev/null +++ b/templates/contact/contact.html @@ -0,0 +1,172 @@ +{% extends 'template.html' %} +{% block css %} + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + + + +
+
+

Contáctanos

+

Déjanos tus datos y nos pondremos en contacto contigo

+
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
+ {{ form.hidden_tag() }} + {{ form.csrf_token }} + + +
+ {{ form.nombre.label(class_="form-label") }} + {{ form.nombre(placeholder="Tu nombre", class_="form-control", maxlength="30") }} +
+ + +
+ {{ form.apellido.label(class_="form-label") }} + {{ form.apellido(placeholder="Tu apellido", class_="form-control", maxlength="50") }} +
+ + +
+ {{ form.email.label(class_="form-label") }} + {{ form.email(placeholder="ejemplo@email.com", class_="form-control", maxlength="35") }} +
+ + +
+ {{ form.estado.label(class_="form-label") }} + {{ form.estado(class_="form-select", maxlength="35") }} +
+ + +
+ {{ form.num_tel.label(class_="form-label") }} + {{ form.num_tel(placeholder="+52 55 1234 5678", class_="form-control", maxlength="16") }} +
+ + +
+ {{ form.size_co.label(class_="form-label") }} + {{ form.size_co(class_="form-select", maxlength="40") }} +
+ + +
+ {{ form.rol_contacto.label(class_="form-label") }} + {{ form.rol_contacto(placeholder="Tu puesto o rol", class_="form-select", maxlength="50") }} +
+ + +
+ {{ form.industry_type.label(class_="form-label") }} + {{ form.industry_type(placeholder="Sector de tu empresa", class_="form-select", maxlength="40") }} +
+ +
+ {{ form.tipo_req.label(class_="form-label") }} + {{ form.tipo_req(placeholder="Describe cómo podemos ayudarte...", class_="form-control", maxlength="250") }} +
+ + +
+
+ +{% endblock body %} \ No newline at end of file diff --git a/templates/home/home.html b/templates/home/home.html new file mode 100644 index 0000000..7f19225 --- /dev/null +++ b/templates/home/home.html @@ -0,0 +1,74 @@ +{% extends 'template.html' %} + +{% block css %} + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + +
+
+ + Potenciamos el talento con soluciones personalizadas que alinean personas, información y propósito. Diseñamos + estrategias basadas en información, ordenada, confiable, disponible y segura, dando FORMHä a tus decisiones. + +
+
+ +
+ +
+
+ Software +
+
+
+ Actualización del proceso de reclutamiento y selección. +
+

+ Actualmente desde la AI hasta las técnicas de entrevista, han abierto un nuevo horizonte en el marco del + reclutamiento y selección de personal… +

+
+
+ + +
+
+ Productividad +
+
+
+ Beneficios e impacto en la productividad. +
+

+ En la medida que adoptemos la NOM 035, factores de riesgos psicosocial en el trabajo, identificación y + prevención; como una guía para optimizar el clima y… +

+
+
+ + +
+
+ Ley Silla +
+
+
+ Alcances y objetivos de la Ley Silla. +
+

+ El pasado 19 de diciembre de 2024, se publico en el Diario Oficial de la Federación el decreto por el cual + se reforman y adicionan diversas… +

+
+
+
+ + + +{% endblock body %} \ No newline at end of file diff --git a/templates/login/login.html b/templates/login/login.html new file mode 100644 index 0000000..97cebc9 --- /dev/null +++ b/templates/login/login.html @@ -0,0 +1,203 @@ +{% extends 'template.html' %} + +{% block css %} +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + + + + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + + +
+ {{ form.hidden_tag() }} + {{ form.csrf_token }} + +

Bienvenido de vuelta

+

Ingresa tus credenciales para continuar

+ +
+ {{ form.email.label(class="form-label") }} +
+ + + + + {{ form.email(class="form-input", placeholder="ejemplo@correo.com", type="email") }} +
+
+ +
+ {{ form.password.label(class="form-label") }} +
+ + + + + {{ form.password(class="form-input", placeholder="••••••••") }} +
+
+ + + + +
+ +{% endblock body %} + +{% block js %} + + + +{% endblock js %} \ No newline at end of file diff --git a/templates/login/recover_pswd.html b/templates/login/recover_pswd.html new file mode 100644 index 0000000..cad2e38 --- /dev/null +++ b/templates/login/recover_pswd.html @@ -0,0 +1,62 @@ +{% extends 'template.html' %} + +{% block css %} +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} +{% endwith %} + + + +
+ {{ form.hidden_tag() }} + {{ form.csrf_token }} + +
+ {{ form.email.label(class="form-label fw-bold") }} + {{ form.email( + class="form-control" + (" is-invalid" if form.email.errors else ""), + **{"aria-describedby": "emailHelp"} + ) }} + + {% if form.email.errors %} +
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
{{ form.email.description }}
+
+ + + +
+ + Regresar al Log In + +
+
+ + +{% endblock body %} + +{% block js %} + +{% endblock js %} \ No newline at end of file diff --git a/templates/methodology/methodology.html b/templates/methodology/methodology.html new file mode 100644 index 0000000..33133d4 --- /dev/null +++ b/templates/methodology/methodology.html @@ -0,0 +1,165 @@ +{% extends 'template.html' %} +{% block css %} + + + + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + + + + +
+
+
+

METODOLOGÍA

+

Evaluamos cada solución para asegurar que generamos valor en tiempo, costos, calidad y/o productividad

+
+ +
+ +
+
+
+ +
+

ANÁLISIS DE DATOS

+

Identificamos información clave que nos permita comprender las necesidades, metas y retos de tu empresa. Así como datos estratégicos.

+
+
+ + +
+
+
+ +
+

PERSONALIZACIÓN

+

Diseñamos e implementamos estrategias personalizadas para potenciar el valor de tu empresa, combinando metodologías especializadas y tecnología.

+
+
+ + +
+
+
+ +
+

EXPERIENCIA DE SERVICIO

+

Aseguraremos que cada solución, proyecto o implementación cuente con una asesoría cercana, con una experiencia de servicio ágil y personalizada tanto para nuestros clientes como usuarios.

+
+
+ + +
+
+
+ +
+

IMPACTO

+

Mostramos resultados tangibles de nuestra intervención con métricas claras y objetivos alcanzables.

+
+
+ + +
+
+
+ +
+

SOSTENIBILIDAD DEL CAMBIO

+

Damos seguimiento a la implementación, reforzando capacidades internas y midiendo avances. Buscamos que cada intervención deje capacidades instaladas y valor.

+
+
+
+
+
+ + + + + + +{% endblock body %} \ No newline at end of file diff --git a/templates/solutions/solutions.html b/templates/solutions/solutions.html new file mode 100644 index 0000000..76f7c0c --- /dev/null +++ b/templates/solutions/solutions.html @@ -0,0 +1,210 @@ +{% extends 'template.html' %} + +{% block css %} + + + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar.html' %} +{% endblock navbar %} + +{% block body %} + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + +{% endblock body %} + +{% block js %} + + + + +{% endblock js %} \ No newline at end of file diff --git a/templates/template.html b/templates/template.html new file mode 100644 index 0000000..b8ab130 --- /dev/null +++ b/templates/template.html @@ -0,0 +1,64 @@ + + + + + + + + {% block title %}{{ title | default('FORMa') }}{% endblock %} + + + + + + + + + + + {% block css %} + {% endblock %} + + + + + + + + + + + + + + + + + + + + + + + {% block navbar %} + {% endblock navbar %} + +
+ {% block body %} + {% endblock body %} +
+ + {% include 'comps/footer.html' %} + + + + + + + + + {% block js %} + {% endblock js %} + + + \ No newline at end of file diff --git a/templates/usr_home/usr_home.html b/templates/usr_home/usr_home.html new file mode 100644 index 0000000..dd23e2c --- /dev/null +++ b/templates/usr_home/usr_home.html @@ -0,0 +1,27 @@ +{% extends 'template.html' %} + +{% block css %} + +{% endblock css %} + +{% block navbar %} +{% include 'comps/navbar_usr.html' %} +{% endblock navbar %} + +{% block body %} + + + + + + + +{% endblock body %} + +{% block js %} + + + +{% endblock js %} \ No newline at end of file