trabajo preliminar, jueves antes de la reunión

This commit is contained in:
David Itehua Xalamihua 2025-03-27 18:07:21 -06:00
parent 1a46da5073
commit 3bb9ac97d2
55 changed files with 2570 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Documents_ref/Oldy.pptx Normal file

Binary file not shown.

42
Documents_ref/conf.sql Normal file
View File

@ -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);

View File

@ -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; } */
}

Binary file not shown.

0
README.md Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

44
forms_py/cls_db.py Normal file
View File

@ -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}")

95
forms_py/cls_db_usr.py Normal file
View File

@ -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}")

View File

@ -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")]
)

View File

@ -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"}
)

View File

@ -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"
}
)

96
forms_py/functions.py Normal file
View File

@ -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

230
main.py
View File

@ -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 = """
<h1>Nuevo contacto recibido</h1>
<p><strong>Nombre:</strong> {} {}</p>
<p><strong>Email:</strong> {}</p>
<p><strong>Teléfono:</strong> {}</p>
<p><strong>Mensaje:</strong> {}</p>
""".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 = """
<h1>Nueva contraseña temporal:</h1>
<p><strong>Contraseña:</strong> {}</p>
<p><strong>Una vez iniciada tu sesión debes de cambiar la contraseña a una nueva que puedas recordar</strong></p>
""".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)

20
requirements.txt Normal file
View File

@ -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

View File

@ -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;
}
}

140
static/home/home.css Normal file
View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

BIN
static/img/home/chair.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
static/img/home/work.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

View File

@ -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;
}
}

View File

@ -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;
}
}

13
static/template/tmp.css Normal file
View File

@ -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;
} */

View File

@ -0,0 +1,78 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='about-us/about-us.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="expanding-panels">
<!-- Quiénes Somos -->
<div class="panel p1">
<div class="panel-content">
<div class="panel-title">
<h3>¿Quiénes Somos?</h3>
</div>
<div class="panel-text">
<p>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º.</p>
<p>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.</p>
<p>Con {% include 'comps/forma.html' %}, tu equipo no solo crece, sino que evoluciona.</p>
</div>
</div>
</div>
<!-- Misión -->
<div class="panel p2">
<div class="panel-content">
<div class="panel-title">
<h3>Misión</h3>
</div>
<div class="panel-text">
<p>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.</p>
<p>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.</p>
</div>
</div>
</div>
<!-- Visión -->
<div class="panel p3">
<div class="panel-content">
<div class="panel-title">
<h3>Visión</h3>
</div>
<div class="panel-text">
<p>Ser la consultoría de referencia en recursos humanos, reconocida por nuestra innovación, precisión y compromiso con la excelencia.</p>
<p>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.</p>
</div>
</div>
</div>
<!-- Valores -->
<div class="panel p4">
<div class="panel-content">
<div class="panel-title">
<h3>Valores</h3>
</div>
<div class="panel-text">
<p>
<ul>
<li><b>Excelencia:</b> Nos involucramos con la calidad en cada servicio y producto que entregamos.</li>
<li><b>Innovación:</b> Adoptamos tecnología de vanguardia para ofrecer soluciones eficientes y competitivas.</li>
<li><b>Responsabilidad:</b> Construimos relaciones de confianza basadas en resultados.</li>
<li><b>Precisión:</b> Brindamos diagnósticos, estrategias y resultados con el más alto grado de exactitud.</li>
<li><b>Ética:</b> Actuamos con integridad y transparencia en cada interacción con nuestros clientes.</li>
</ul>
</p>
</div>
</div>
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,73 @@
<style>
/* Estilos personalizados para el footer */
.footer {
background-color: #50164A ; /* Color de fondo oscuro */
color: #ffffff; /* Color del texto */
padding: 20px 0;
font-size: 20px;
}
</style>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="row">
<!-- Columna 1: Información de contacto -->
<div class="col-md-6 text-center">
<h5>Contacto</h5>
<button type="button" class="btn btn-light">
<i class="bi bi-telephone-fill"></i> +52 5480-xxxx
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #000000;" width="16" height="16" fill="currentColor" class="bi bi-envelope-fill" viewBox="0 0 16 16">
<path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414zM0 4.697v7.104l5.803-3.558zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586zm3.436-.586L16 11.801V4.697z"/>
</svg>
</button>
</div>
<!-- Columna 2: Enlaces útiles -->
<div class="col-md-6 text-center">
<h5>Redes Sociales</h5>
<div class="r-i-i">
<!-- <button onclick="window.location.href='{{ url_for('home') }}'"> </button> -->
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #3b5998;" width="16" height="16" fill="currentColor" class="bi bi-facebook" viewBox="0 0 16 16">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #000000;" width="16" height="16" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #F56040;" width="16" height="16" fill="currentColor" class="bi bi-instagram" viewBox="0 0 16 16">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.9 3.9 0 0 0-1.417.923A3.9 3.9 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.9 3.9 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.9 3.9 0 0 0-.923-1.417A3.9 3.9 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599s.453.546.598.92c.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.5 2.5 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.5 2.5 0 0 1-.92-.598 2.5 2.5 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233s.008-2.388.046-3.231c.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92s.546-.453.92-.598c.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92m-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217m0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #FF0000;" width="16" height="16" fill="currentColor" class="bi bi-youtube" viewBox="0 0 16 16">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.01 2.01 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.01 2.01 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31 31 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.01 2.01 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A100 100 0 0 1 7.858 2zM6.4 5.209v4.818l4.157-2.408z"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #0e76a8;" width="16" height="16" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Derechos de autor -->
<div class="row mt-4">
<div class="col-12 text-center">
<p class="mb-0">&copy; 2025{% include 'comps/forma.html' %}. Todos los derechos reservados.</p>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1 @@
<span style="color: #747474;">FORM<span style="color: #47D45A;">H</span><span style="color: #0F9ED5;">ä</span></span>

View File

@ -0,0 +1,57 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-custom">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">
{% include 'comps/forma.html' %}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 effect-3">
<!-- Inicio -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" aria-current="page" href="{{ url_for('home') }}">Inicio</a>
</li>
<!-- Nosotros -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'about_us' %}active{% endif %}" href="{{ url_for('about_us') }}">Nosotros</a>
</li>
<!-- Soluciones -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'solutions' %}active{% endif %}" href="{{ url_for('solutions') }}">Soluciones</a>
</li>
<!-- Metodología -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'methodology' %}active{% endif %}" href="{{ url_for('methodology') }}">Metodología</a>
</li>
<!-- Testimoniales -->
<!-- <li class="nav-item">
<a class="nav-link" href="#">Testimoniales</a>
</li> -->
<!-- Blog -->
<!-- <li class="nav-item">
<a class="nav-link" href="#">Blog</a>
</li> -->
<!-- Contacto -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'contact' %}active{% endif %}" href="{{url_for('contact')}}">Contacto</a>
</li>
<!-- Log In -->
<li class="nav-item">
<a class="nav-link {% if active_page == 'login' %}active{% endif %}" href="{{url_for('login')}}">Log In</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@ -0,0 +1,34 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-custom">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">
{% include 'comps/forma.html' %}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 effect-3">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> Usuario
</a>
<ul class="dropdown-menu">
<!-- <li><a class="dropdown-item" href="#">Another action</a></li> -->
<li><hr class="dropdown-divider"></li>
<!-- <li><a class="dropdown-item" href="#">Something else here</a></li> -->
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="bi bi-door-open-fill"></i> Logout
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Asegúrate de tener esto al final del body -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -->

View File

@ -0,0 +1,172 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
.form-container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
font-family: 'Segoe UI', Roboto, sans-serif;
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-header {
text-align: center;
margin-bottom: 1.5rem;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 1.2rem;
}
.form-label {
flex: 0 0 180px;
text-align: left;
font-weight: 500;
color: #2d3748;
}
.form-control {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 1rem;
transition: all 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.btn-primary {
display: block;
width: 100%;
max-width: 200px;
margin: 1.5rem auto 0;
padding: 0.75rem;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary:hover {
background-color: #3182ce;
}
</style>
<div class="form-container">
<div class="form-header">
<h2>Contáctanos</h2>
<p>Déjanos tus datos y nos pondremos en contacto contigo</p>
</div>
<!-- Mensajes Flash -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('contact') }}">
{{ form.hidden_tag() }}
{{ form.csrf_token }}
<!-- {# Nombre #} -->
<div class="form-row">
{{ form.nombre.label(class_="form-label") }}
{{ form.nombre(placeholder="Tu nombre", class_="form-control", maxlength="30") }}
</div>
<!-- {# Apellido #} -->
<div class="form-row">
{{ form.apellido.label(class_="form-label") }}
{{ form.apellido(placeholder="Tu apellido", class_="form-control", maxlength="50") }}
</div>
<!-- {# Email #} -->
<div class="form-row">
{{ form.email.label(class_="form-label") }}
{{ form.email(placeholder="ejemplo@email.com", class_="form-control", maxlength="35") }}
</div>
<!-- {# Estado #} -->
<div class="form-row">
{{ form.estado.label(class_="form-label") }}
{{ form.estado(class_="form-select", maxlength="35") }}
</div>
<!-- {# Número telefónico #} -->
<div class="form-row">
{{ form.num_tel.label(class_="form-label") }}
{{ form.num_tel(placeholder="+52 55 1234 5678", class_="form-control", maxlength="16") }}
</div>
<!-- {# Tamaño de la compañia #} -->
<div class="form-row">
{{ form.size_co.label(class_="form-label") }}
{{ form.size_co(class_="form-select", maxlength="40") }}
</div>
<!-- {# rol del contacto dentro de la empresa #} -->
<div class="form-row">
{{ form.rol_contacto.label(class_="form-label") }}
{{ form.rol_contacto(placeholder="Tu puesto o rol", class_="form-select", maxlength="50") }}
</div>
<!-- {# Sector de la empresa #} -->
<div class="form-row">
{{ form.industry_type.label(class_="form-label") }}
{{ form.industry_type(placeholder="Sector de tu empresa", class_="form-select", maxlength="40") }}
</div>
<div class="form-row">
{{ form.tipo_req.label(class_="form-label") }}
{{ form.tipo_req(placeholder="Describe cómo podemos ayudarte...", class_="form-control", maxlength="250") }}
</div>
<button type="submit" class="btn btn-primary" >
Enviar Mensaje
<i class="bi bi-send-check-fill"></i>
</button>
</form>
</div>
{% endblock body %}

74
templates/home/home.html Normal file
View File

@ -0,0 +1,74 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='home/home.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="r-i">
<div class="r-i-ii">
<spam>
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.
</spam>
</div>
</div>
<div class="r-ii">
<!-- Tarjeta 1 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='img/home/software.avif')}}" alt="Software">
</div>
<div class="text-container">
<h6>
Actualización del proceso de reclutamiento y selección.
</h6>
<p>
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…
</p>
</div>
</div>
<!-- Tarjeta 2 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='img/home/work.avif')}}" alt="Productividad">
</div>
<div class="text-container">
<h6>
Beneficios e impacto en la productividad.
</h6>
<p>
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…
</p>
</div>
</div>
<!-- Tarjeta 3 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='img/home/chair.avif')}}" alt="Ley Silla">
</div>
<div class="text-container">
<h6>
Alcances y objetivos de la Ley Silla.
</h6>
<p>
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…
</p>
</div>
</div>
</div>
{% endblock body %}

203
templates/login/login.html Normal file
View File

@ -0,0 +1,203 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
:root {
--primary-color: #4361ee;
--primary-dark: #3a56d4;
--text-color: #2b2d42;
--light-gray: #f8f9fa;
--white: #ffffff;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.login-form {
max-width: 400px;
margin: 2rem auto;
padding: 2.5rem;
background: var(--white);
border-radius: 12px;
box-shadow: var(--shadow);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.form-title {
color: var(--text-color);
text-align: center;
margin-bottom: 0.5rem;
font-size: 1.8rem;
}
.form-subtitle {
color: #6c757d;
text-align: center;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-color);
font-weight: 500;
font-size: 0.9rem;
}
.input-container {
position: relative;
}
.input-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
color: #6c757d;
}
.form-input {
width: 100%;
padding: 12px 12px 12px 40px;
border: 1px solid #ced4da;
border-radius: 8px;
font-size: 1rem;
transition: all 0.3s ease;
background-color: var(--light-gray);
}
.form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
}
.submit-btn {
width: 100%;
padding: 12px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.submit-btn:hover {
background-color: var(--primary-dark);
transform: translateY(-2px);
}
.btn-icon {
width: 18px;
height: 18px;
}
.form-footer {
margin-top: 1.5rem;
text-align: center;
font-size: 0.9rem;
color: #6c757d;
}
.forgot-password {
display: block;
margin-bottom: 1rem;
color: var(--primary-color);
text-decoration: none;
transition: color 0.2s;
}
.forgot-password:hover {
text-decoration: underline;
}
.signup-link {
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
.signup-link:hover {
text-decoration: underline;
}
</style>
<!-- En tu template (frontend) -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('login') }}" class="login-form">
{{ form.hidden_tag() }}
{{ form.csrf_token }}
<h2 class="form-title">Bienvenido de vuelta</h2>
<p class="form-subtitle">Ingresa tus credenciales para continuar</p>
<div class="form-group">
{{ form.email.label(class="form-label") }}
<div class="input-container">
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
{{ form.email(class="form-input", placeholder="ejemplo@correo.com", type="email") }}
</div>
</div>
<div class="form-group">
{{ form.password.label(class="form-label") }}
<div class="input-container">
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
{{ form.password(class="form-input", placeholder="••••••••") }}
</div>
</div>
<button type="submit" class="submit-btn">
Iniciar Sesión
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="btn-icon">
<path d="M5 12h14M12 5l7 7-7 7"></path>
</svg>
</button>
<div class="form-footer">
<a href="{{ url_for('recover_pswd') }}" class="forgot-password">¿Olvidaste tu contraseña?</a>
</div>
</form>
{% endblock body %}
{% block js %}
{% endblock js %}

View File

@ -0,0 +1,62 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<!-- En tu template (frontend) -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('recover_pswd') }}" class="w-100 mx-auto p-4 border rounded shadow" novalidate>
{{ form.hidden_tag() }}
{{ form.csrf_token }}
<div class="mb-3">
{{ 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 %}
<div class="invalid-feedback">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div id="emailHelp" class="form-text">{{ form.email.description }}</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
<i class="bi bi-envelope-fill me-2"></i> Recuperar contraseña
</button>
<div class="text-center mt-3">
<a href="{{ url_for('login') }}" class="text-decoration-none">
<i class="bi bi-arrow-left me-1"></i> Regresar al Log In
</a>
</div>
</form>
{% endblock body %}
{% block js %}
{% endblock js %}

View File

@ -0,0 +1,165 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='methodology/methodology.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
.methodology-section {
padding: 5rem 0;
background-color: #f9fafb;
}
.section-title {
text-align: center;
margin-bottom: 4rem;
}
.section-title h2 {
font-weight: 600;
color: #2d3748;
margin-bottom: 1rem;
position: relative;
display: inline-block;
}
.section-title h2:after {
content: '';
display: block;
width: 50px;
height: 3px;
background: #4f46e5;
margin: 10px auto 0;
}
.section-title .lead {
color: #4a5568;
max-width: 700px;
margin: 0 auto;
font-size: 1.1rem;
}
.methodology-card {
background: white;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
transition: all 0.3s ease;
height: 100%;
border-left: 4px solid #4f46e5;
}
.methodology-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
.card-icon {
font-size: 2rem;
color: #4f46e5;
margin-bottom: 1.5rem;
}
.card-title {
font-weight: 600;
color: #2d3748;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.card-text {
color: #4a5568;
line-height: 1.6;
}
.highlight-card {
border-left-color: #10b981;
}
.highlight-card .card-icon {
color: #10b981;
}
</style>
<section class="methodology-section">
<div class="container">
<div class="section-title">
<h2>METODOLOGÍA</h2>
<p class="lead">Evaluamos cada solución para asegurar que generamos valor en tiempo, costos, calidad y/o productividad</p>
</div>
<div class="row">
<!-- Tarjeta 1 -->
<div class="col-md-6 col-lg-4">
<div class="methodology-card">
<div class="card-icon">
<i class="bi bi-bar-chart-line"></i>
</div>
<h3 class="card-title">ANÁLISIS DE DATOS</h3>
<p class="card-text">Identificamos información clave que nos permita comprender las necesidades, metas y retos de tu empresa. Así como datos estratégicos.</p>
</div>
</div>
<!-- Tarjeta 2 -->
<div class="col-md-6 col-lg-4">
<div class="methodology-card highlight-card">
<div class="card-icon">
<i class="bi bi-sliders"></i>
</div>
<h3 class="card-title">PERSONALIZACIÓN</h3>
<p class="card-text">Diseñamos e implementamos estrategias personalizadas para potenciar el valor de tu empresa, combinando metodologías especializadas y tecnología.</p>
</div>
</div>
<!-- Tarjeta 3 -->
<div class="col-md-6 col-lg-4">
<div class="methodology-card">
<div class="card-icon">
<i class="bi bi-people"></i>
</div>
<h3 class="card-title">EXPERIENCIA DE SERVICIO</h3>
<p class="card-text">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.</p>
</div>
</div>
<!-- Tarjeta 4 -->
<div class="col-md-6 col-lg-4">
<div class="methodology-card">
<div class="card-icon">
<i class="bi bi-graph-up-arrow"></i>
</div>
<h3 class="card-title">IMPACTO</h3>
<p class="card-text">Mostramos resultados tangibles de nuestra intervención con métricas claras y objetivos alcanzables.</p>
</div>
</div>
<!-- Tarjeta 5 -->
<div class="col-md-6 col-lg-4">
<div class="methodology-card">
<div class="card-icon">
<i class="bi bi-arrow-repeat"></i>
</div>
<h3 class="card-title">SOSTENIBILIDAD DEL CAMBIO</h3>
<p class="card-text">Damos seguimiento a la implementación, reforzando capacidades internas y midiendo avances. Buscamos que cada intervención deje capacidades instaladas y valor.</p>
</div>
</div>
</div>
</div>
</section>
{% endblock body %}

View File

@ -0,0 +1,210 @@
{% extends 'template.html' %}
{% block css %}
<!-- {# modal bootstrap #} -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{url_for('static', filename='solutions/solutions.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="parent">
<div class="i-i" data-toggle="modal" data-target="#forma-reclutamiento"></div>
<div class="i-ii" data-toggle="modal" data-target="#forma-liderazgo"></div>
<div class="i-iii" data-toggle="modal" data-target="#forma-capacitacion"></div>
<div class="ii-i" data-toggle="modal" data-target="#forma-cambio"></div>
<div class="ii-ii" data-toggle="modal" data-target="#forma-objetivos"></div>
</div>
<!-- da forma reclutamiento -->
<div class="modal fade" id="forma-reclutamiento" tabindex="-1" role="dialog" aria-labelledby="forma-reclutamientoTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Impulsa tu equipo con un reclutamiento más preciso, ágil y estratégico.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
En {% include 'comps/forma.html' %}, combinamos tecnología y metodologías avanzadas para atraer, identificar y seleccionar talento de alto
potencial.
<ul>
<li><b>Plataformas digitales e inteligencia artificial:</b> Utilizamos herramientas especializadas, para optimizar
la búsqueda y selección de candidatos.</li>
<li><b>Entrevistas estructuradas y por competencias:</b> Aplicamos diversas técnicas, como entrevistas iniciales,
por competencias, modelo SMART y entrevistas profundas para evaluar habilidades, experiencia y valores.</li>
<li><b>Evaluaciones psicométricas:</b> Medimos aptitudes cognitivas, rasgos de personalidad y estilos de liderazgo
para garantizar el ajuste ideal al puesto.</li>
<li><b>Assessment Center:</b> Implementamos dinámicas grupales, simulaciones y ejercicios de toma de decisiones
para
evaluar el desempeño real en situaciones laborales.</li>
<li><b>Estudios socioeconómicos digitales:</b> Realizamos verificaciones detalladas mediante plataformas digitales
para confirmar antecedentes laborales, referencias y nivel socioeconómico, asegurando procesos confiables y
ágiles.</li>
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
</div>
</div>
</div>
<!-- da forma a tu liderazgo-->
<div class="modal fade" id="forma-liderazgo" tabindex="-1" role="dialog" aria-labelledby="forma-liderazgoTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'comps/forma.html' %} a tu liderazgo.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
Prepara a tus líderes para afrontar los desafíos del entorno empresarial actual con estrategias que potencian su
crecimiento y efectividad. En {% include 'comps/forma.html' %}, diseñamos soluciones para identificar, desarrollar y fortalecer el liderazgo
en todos los niveles de la organización. <br>
Nos especializamos en:
<ul>
<li>Mentorías de talento femenino.</li>
<li>Assessment.</li>
<li>Planes de desarrollo de talento.</li>
<li>Nine Box.</li>
<li>Programas de liderazgo especializado.</li>
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
</div>
</div>
</div>
<!-- da forma a tu capacitación-->
<div class="modal fade" id="forma-capacitacion" tabindex="-1" role="dialog" aria-labelledby="forma-capacitacionTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'comps/forma.html' %} a tu capacitación. </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<p>
Impulsamos el desarrollo de habilidades como clave del crecimiento empresarial, promoviendo una cultura de
aprendizaje, en donde cada colaborador asume un rol activo en su evolución profesional, cómo lo hacemos:
</p>
<p>
Análisis de necesidades: Identificamos los retos específicos de tu equipo para diseñar programas a la medida.
<ul>
<li>
<b>Contenidos teórico-prácticos:</b> Estructuramos materiales que combinan teoría con aplicación real.
</li>
<li>
<b>Metodologías innovadoras:</b> Aplicamos enfoques de vanguardia para maximizar el aprendizaje, tales como;
Microlearning, Gamificación, E-learning y Blended Learning
</li>
</ul>
Nuestro Top10 de formación:
<ul>
<li>Liderazgo y gestión de equipos.</li>
<li>Desarrollo de up, soft y hard skills.</li>
<li>Gestión del cambio.</li>
<li>Transformación digital y competencias tecnológicas.</li>
<li>Metodologías ágiles y gestión de proyectos.</li>
<li>Servicio y experiencia del cliente.</li>
<li>Diversidad, equidad e inclusión.</li>
<li>Accesibilidad.</li>
<li>Seguridad y bienestar laboral.</li>
<li>Temas específicos y especializados en Capital Humano.</li>
</ul>
Ya sea en capacitaciones, escuelas o universidades, contamos con experiencia en diversas estructuras
</p>
</div>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
</div>
</div>
</div>
<!-- da forma al cambio-->
<div class="modal fade" id="forma-cambio" tabindex="-1" role="dialog" aria-labelledby="forma-cambioTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Simplifica, optimiza y fortalece tu capital humano con asesoría especializada.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
Facilitamos en las organizaciones la optimización y simplificación de sus procesos mediante un enfoque
estratégico, metodologías ágiles y soluciones tecnológicas. Nuestras soluciones incluyen:
<ul>
<li>Uso estratégico de tecnología en los procesos</li>
<li>Estudios y estrategias para optimización de la eficiencia operativa</li>
<li>Desarrollo e implementación de normas, políticas y procedimientos</li>
<li>Desarrollo y optimización de áreas de Capital Humano</li>
<li>Diseño de estructuras organizacionales. Trazabilidad de roles, responsabilidades y procesos clave.</li>
<li>Asesoría laboral y legal</li>
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
</div>
</div>
</div>
<!-- da forma a tus objetivos-->
<div class="modal fade" id="forma-objetivos" tabindex="-1" role="dialog" aria-labelledby="forma-objetivosTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Convierte datos en decisiones estratégicas para impulsar el crecimiento de tu empresa</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
Transformamos la información en estrategias accionables. Nuestras soluciones se basan en la recolección y análisis de datos, utilizando herramientas digitales y metodologías estadísticas para generar diagnósticos precisos y planes de acción estratégicos.
<ul>
<li>Detección de Necesidades de Capacitación.</li>
<li>Evaluación de engagement, clima y cultura.</li>
<li>Evaluación de Desempeño</li>
<li>Integración con la NOM 035-STPS-2018.</li>
<li>Intervenciones grupales e individuales de Desarrollo Organizacional (DO)</li>
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
</div>
</div>
</div>
{% endblock body %}
{% block js %}
<!-- 2. Al final del BODY (en este orden) -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
{% endblock js %}

64
templates/template.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% block title %}{{ title | default('FORMa') }}{% endblock %}
</title>
<!-- {# i bootstrap #} -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- {# icons #}} -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- {# f bootstrap #} -->
{% block css %}
{% endblock %}
<!-- {# i navbar css #} -->
<link rel="stylesheet" href="{{url_for('static', filename='template/tmp.css')}}">
<!-- {# navbar #} -->
<link rel="stylesheet" href="{{url_for('static', filename='template/navbar.css')}}">
<!-- {# f navbar css #} -->
<!-- {# i notify css #} -->
<!-- CSS -->
<!-- {# doc: https://github.com/simple-notify/simple-notify?tab=readme-ov-file #} -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-notify/dist/simple-notify.css" />
<!-- {# f notify css #} -->
<!-- {# i notify js #} -->
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/simple-notify/dist/simple-notify.min.js"></script>
<!-- {# f notify js #} -->
</head>
<!-- <body> -->
<body>
{% block navbar %}
{% endblock navbar %}
<!-- <main> -->
<main>
{% block body %}
{% endblock body %}
</main>
{% include 'comps/footer.html' %}
<!-- {# i bootstrap js #} -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- {# f bootstrap js #} -->
{% block js %}
{% endblock js %}
</body>
</html>

View File

@ -0,0 +1,27 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar_usr.html' %}
{% endblock navbar %}
{% block body %}
<!-- {% if current_user %}
<h2>Bienvenido, {{ current_user }}</h2>
{% endif %} -->
{% endblock body %}
{% block js %}
{% endblock js %}