diff --git a/.gitignore b/.gitignore index 80704f4378..bf5fa06762 100755 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ database.database database.db diagram.png __pycache__/ +migrations/ +Pipfile.lock +package-lock.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 246b0419d0..50e5961d29 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,11 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "workbench.editorAssociations": { - "*.md": "vscode.markdown.preview.editor" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - } + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + }, + "python.analysis.extraPaths": ["./src/api"] } diff --git a/Pipfile b/Pipfile index 44e04f14ff..3d2074fb0c 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-mail = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index b201c3decc..6b31c0eff3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2e672e650278aeeee2fe49bd76d76497d8b65a50f8b5dbb121d265cbc6ef4e5" + "sha256": "3f984adbcb33f8bdadc9347099a43d8924f246a3077b42cc7f4acfc3b9b80596" }, "pipfile-spec": 6, "requires": { @@ -42,11 +42,11 @@ }, "click": { "hashes": [ - "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", - "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" ], - "markers": "python_version >= '3.7'", - "version": "==8.1.8" + "markers": "python_version >= '3.10'", + "version": "==8.2.1" }, "cloudinary": { "hashes": [ @@ -58,11 +58,12 @@ }, "flask": { "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" + "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", + "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e" ], "index": "pypi", - "version": "==3.1.0" + "markers": "python_version >= '3.9'", + "version": "==3.1.1" }, "flask-admin": { "hashes": [ @@ -88,6 +89,15 @@ "index": "pypi", "version": "==4.6.0" }, + "flask-mail": { + "hashes": [ + "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", + "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.10.0" + }, "flask-migrate": { "hashes": [ "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", @@ -209,11 +219,11 @@ }, "jinja2": { "hashes": [ - "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", - "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" ], "markers": "python_version >= '3.7'", - "version": "==3.1.5" + "version": "==3.1.6" }, "mako": { "hashes": [ diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..b7266ba201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "react-router-hash-link": "^2.4.3" }, "devDependencies": { "@types/react": "^18.2.18", @@ -3553,6 +3554,19 @@ "react-dom": ">=16.8" } }, + "node_modules/react-router-hash-link": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz", + "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router-dom": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6743,6 +6757,14 @@ "react-router": "6.29.0" } }, + "react-router-hash-link": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz", + "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==", + "requires": { + "prop-types": "^15.7.2" + } + }, "reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", diff --git a/package.json b/package.json index 0caab10749..c0c7ce804d 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -55,8 +55,9 @@ }, "dependencies": { "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0", + "react-router-hash-link": "^2.4.3" } } diff --git a/src/api/admin.py b/src/api/admin.py index 3eecb64140..1a83ba17a9 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,17 +1,42 @@ - + import os from flask_admin import Admin -from .models import db, User +from api.models import db, User, Orden_de_trabajo, Vehiculos, Servicio, AuxOrdenServicio from flask_admin.contrib.sqla import ModelView + +class OrdenTrabajoModelView(ModelView): + column_auto_selected_related = True + column_list = ['id_ot', 'fecha_ingreso', 'estado_servicio', 'fecha_final', + 'usuario_id', 'vehiculo_id', 'mecanico_id', 'cliente', 'mecanico', 'vehiculo', 'servicios_asociados'] + + +class vehiculosModelView(ModelView): + column_auto_selected_related = True + column_list = ['id_vehiculo', 'matricula', 'marca', + 'modelo', 'year', 'user_id', 'user', 'ordenes_trabajo'] + + +class servicioModelView(ModelView): + column_auto_selected_related = True + column_list = ['id_service', 'name_service', 'price', 'ordenes_asociadas'] + + +class auxOrdenServicioModelView(ModelView): + column_auto_selected_related = True + column_list = ['id', 'orden_id', 'orden', 'servicio_id', 'servicio'] + + def setup_admin(app): app.secret_key = os.environ.get('FLASK_APP_KEY', 'sample key') app.config['FLASK_ADMIN_SWATCH'] = 'cerulean' - admin = Admin(app, name='4Geeks Admin', template_mode='bootstrap3') + admin = Admin(app, name='AutoTek Admin', template_mode='bootstrap3') - # Add your models here, for example this is how we add a the User model to the admin admin.add_view(ModelView(User, db.session)) - + admin.add_view(OrdenTrabajoModelView(Orden_de_trabajo, db.session)) + admin.add_view(vehiculosModelView(Vehiculos, db.session)) + admin.add_view(servicioModelView(Servicio, db.session)) + admin.add_view(auxOrdenServicioModelView(AuxOrdenServicio, db.session)) # You can duplicate that line to add mew models - # admin.add_view(ModelView(YourModelName, db.session)) \ No newline at end of file + # admin.add_view(ModelView(YourModelName, db.session)) diff --git a/src/api/commands.py b/src/api/commands.py index 19806164d3..d202af967c 100644 --- a/src/api/commands.py +++ b/src/api/commands.py @@ -1,11 +1,10 @@ - import click -from api.models import db, User +from .models import db, User """ In this file, you can add as many commands as you want using the @app.cli.command decorator -Flask commands are usefull to run cronjobs or tasks outside of the API but sill in integration -with youy database, for example: Import the price of bitcoin every night as 12am +Flask commands are useful to run cronjobs or tasks outside of the API but still in integration +with your database, for example: Import the price of bitcoin every night at 12am """ def setup_commands(app): @@ -15,7 +14,7 @@ def setup_commands(app): Note: 5 is the number of users to add """ @app.cli.command("insert-test-users") # name of our command - @click.argument("count") # argument of out command + @click.argument("count") # argument of our command def insert_test_users(count): print("Creating test users") for x in range(1, int(count) + 1): @@ -31,4 +30,4 @@ def insert_test_users(count): @app.cli.command("insert-test-data") def insert_test_data(): - pass \ No newline at end of file + pass diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..7a7efe9209 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,180 @@ from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Boolean, Integer, Enum, Date, Numeric, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +import enum +import datetime +from typing import List db = SQLAlchemy() -class User(db.Model): + +# Se definen los posibles estados de la orden de servicio con tipo de datos ENUM + +class status(enum.Enum): + INGRESADO = 'Ingresado' + EN_PROCESO = 'En proceso' + FINALIZADO = 'Finalizado' + + +class Orden_de_trabajo(db.Model): + __tablename__ = "orden_de_trabajo" + id_ot: Mapped[int] = mapped_column(primary_key=True) + #nombre_cliente: Mapped[str] = mapped_column(String(80), nullable=False) + fecha_ingreso: Mapped[datetime.date] = mapped_column(Date, nullable=False) + estado_servicio: Mapped[status] = mapped_column( + Enum(status, name="estado_orden"), nullable=False) + fecha_final: Mapped[datetime.date] = mapped_column(Date, nullable=True) + + usuario_id: Mapped[int] = mapped_column( + ForeignKey("user.id_user"), nullable=False) + vehiculo_id: Mapped[int] = mapped_column( + ForeignKey("vehiculos.id_vehiculo"), nullable=False) + mecanico_id: Mapped[int] = mapped_column( + ForeignKey("user.id_user"), nullable=False) + + # RELACIONES CON OTRAS TABLAS + cliente: Mapped["User"] = relationship( + foreign_keys=[usuario_id], back_populates="ordenes_cliente") + mecanico: Mapped["User"] = relationship( + foreign_keys=[mecanico_id], back_populates="ordenes_mecanico") + vehiculo: Mapped["Vehiculos"] = relationship( + back_populates="ordenes_trabajo") + servicios_asociados: Mapped[list['AuxOrdenServicio']] = relationship( + back_populates="orden") + + def __str__(self): + return f'{self.id_ot}' + + def serialize(self): + return { + 'id_ot': self.id_ot, + #'nombre_cliente': self.nombre_cliente, + 'fecha_ingreso': self.fecha_ingreso, + 'estado_servicio': self.estado_servicio.value, + 'fecha_final': self.fecha_final, + 'usuario_id': self.usuario_id, + 'vehiculo_id': self.vehiculo_id, + 'mecanico_id': self.mecanico_id, + 'nombre_mecanico': self.mecanico.nombre, + 'matricula_vehiculo': self.vehiculo.matricula, + 'servicios_asociados': [s.serialize() for s in self.servicios_asociados] + + } + + +class AuxOrdenServicio(db.Model): + __tablename__ = "auxOrdenServicio" id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + orden_id: Mapped[int] = mapped_column( + ForeignKey("orden_de_trabajo.id_ot"), nullable=False) + orden: Mapped['Orden_de_trabajo'] = relationship( + back_populates='servicios_asociados') + servicio_id: Mapped[int] = mapped_column( + ForeignKey("servicio.id_service"), nullable=False) + servicio: Mapped['Servicio'] = relationship( + back_populates="ordenes_asociadas") + + def __str__(self): + return f'{self.servicio}' + + def serialize(self): + return { + 'id': self.id, + 'orden_id': self.orden_id, + 'servicio_id': self.servicio_id, + 'servicio': self.servicio.serialize() # accede al servicio completo + } + + +class RolEnum(enum.Enum): + MECANICO = 'Mecanico' + CLIENTE = 'Cliente' +class User(db.Model): + __tablename__ = "user" + id_user: Mapped[int] = mapped_column(primary_key=True) + nombre: Mapped[str] = mapped_column(String(80), nullable=False) + identificacion: Mapped[int] = mapped_column(Integer, unique=True, nullable=False) + password: Mapped[str] = mapped_column(String(300), nullable=False) # ✅ ahora soporta hash + telefono: Mapped[str] = mapped_column(String(11)) + email: Mapped[str] = mapped_column(String(30), unique=True, nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, nullable=False) + foto_usuario: Mapped[str] = mapped_column(String(255), nullable=True) + rol: Mapped[RolEnum] = mapped_column(Enum(RolEnum, name="rol_enum"), nullable=False) + + # RELACIONES CON OTRAS TABLAS + vehiculos: Mapped[List["Vehiculos"]] = relationship(back_populates="user") + ordenes_cliente: Mapped[List["Orden_de_trabajo"]] = relationship( + back_populates="cliente", foreign_keys="Orden_de_trabajo.usuario_id") + ordenes_mecanico: Mapped[List["Orden_de_trabajo"]] = relationship( + back_populates="mecanico", foreign_keys="Orden_de_trabajo.mecanico_id") + + def __str__(self): + return f'{self.nombre}' + def serialize(self): return { - "id": self.id, - "email": self.email, - # do not serialize the password, its a security breach - } \ No newline at end of file + 'id_user': self.id_user, + 'nombre': self.nombre, + 'identificacion': self.identificacion, + 'telefono': self.telefono, + 'email': self.email, + 'is_active': self.is_active, + 'foto_usuario': self.foto_usuario, + 'rol': self.rol.value + # 🔒 no incluir password en serialize + } + + + +class Vehiculos(db.Model): + __tablename__ = 'vehiculos' + id_vehiculo: Mapped[int] = mapped_column(primary_key=True) + matricula: Mapped[str] = mapped_column( + String(8), unique=True, nullable=False) + marca: Mapped[str] = mapped_column(String(15), nullable=False) + modelo: Mapped[str] = mapped_column(String(15), nullable=False) + year: Mapped[int] = mapped_column(Integer, nullable=False) + user_id: Mapped[int] = mapped_column( + ForeignKey("user.id_user"), nullable=False) + + # RELACIONES + user: Mapped["User"] = relationship(back_populates="vehiculos") + ordenes_trabajo: Mapped[list["Orden_de_trabajo"] + ] = relationship(back_populates="vehiculo") + + + def __str__(self): + return f'{self.matricula}' + + def serialize(self): + return { + 'id_vehiculo': self.id_vehiculo, + 'matricula': self.matricula, + 'marca': self.marca, + 'modelo': self.modelo, + 'year': self.year, + 'user_id': self.user_id + } + + +class Servicio(db.Model): + __tablename__ = 'servicio' + id_service: Mapped[int] = mapped_column(primary_key=True) + name_service: Mapped[str] = mapped_column(String(100), nullable=False) + price: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False) + # RELACIONES CON OTRAS TABLAS + ordenes_asociadas: Mapped[List["AuxOrdenServicio"] + ] = relationship(back_populates="servicio") + + def __str__(self): + return f'{self.name_service}' + + def serialize(self): + return { + 'ide_service': self.id_service, + 'name_service': self.name_service, + 'price': self.price + } + diff --git a/src/app.py b/src/app.py index 1b3340c0fa..08376fa5b8 100644 --- a/src/app.py +++ b/src/app.py @@ -6,20 +6,36 @@ from flask_migrate import Migrate from flask_swagger import swagger from api.utils import APIException, generate_sitemap -from api.models import db +from api.models import db, User, Orden_de_trabajo, Vehiculos, Servicio, AuxOrdenServicio, RolEnum + +from datetime import timedelta + from api.routes import api from api.admin import setup_admin from api.commands import setup_commands -# from models import Person +from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager +from flask_cors import CORS +from flask_mail import Mail, Message +import random +from werkzeug.security import generate_password_hash + +# 🔹 Variable global para almacenar códigos de recuperación temporalmente +reset_codes = {} +verified_emails = {} ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" static_file_dir = os.path.join(os.path.dirname( os.path.realpath(__file__)), '../dist/') app = Flask(__name__) +CORS(app) + +app.config["JWT_SECRET_KEY"] = os.getenv('JWT_KEY') +jwt = JWTManager(app) + app.url_map.strict_slashes = False -# database condiguration +# Database configuration db_url = os.getenv("DATABASE_URL") if db_url is not None: app.config['SQLALCHEMY_DATABASE_URI'] = db_url.replace( @@ -31,23 +47,31 @@ MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) -# add the admin +# Admin y comandos setup_admin(app) - -# add the admin setup_commands(app) -# Add all endpoints form the API with a "api" prefix +# Blueprints app.register_blueprint(api, url_prefix='/api') -# Handle/serialize errors like a JSON object +# 🔹 Configuración de Flask-Mail +app.config['MAIL_SERVER'] = 'smtp.gmail.com' +app.config['MAIL_PORT'] = 587 +app.config['MAIL_USE_TLS'] = True +app.config['MAIL_USE_SSL'] = False +app.config['MAIL_USERNAME'] = 'pruebaautotek@gmail.com' # tu correo +app.config['MAIL_PASSWORD'] = 'hzyp ztmh iteh bevk' # tu App Password +app.config['MAIL_DEFAULT_SENDER'] = ('Soporte AutoTek', 'tucorreo@gmail.com') +mail = Mail(app) + +# 🔹 Manejo de errores @app.errorhandler(APIException) def handle_invalid_usage(error): return jsonify(error.to_dict()), error.status_code -# generate sitemap with all your endpoints +# 🔹 Sitemap @app.route('/') @@ -56,17 +80,185 @@ def sitemap(): return generate_sitemap(app) return send_from_directory(static_file_dir, 'index.html') -# any other endpoint will try to serve it like a static file + @app.route('/', methods=['GET']) def serve_any_other_file(path): if not os.path.isfile(os.path.join(static_file_dir, path)): path = 'index.html' response = send_from_directory(static_file_dir, path) - response.cache_control.max_age = 0 # avoid cache memory + response.cache_control.max_age = 0 return response +# ---------------------- ENDPOINTS DEL PROYECTO ---------------------- + +# ENDPOINT PARA TRAER ORDENES DE TRABAJO + +@app.route('/ordenes_de_trabajo', methods=['GET']) +@jwt_required() +def get_orden_de_trabajo(): + email_user_current = get_jwt_identity() + user_current = User.query.filter_by(email=email_user_current).first() + id_propietario = user_current.id_user + + rol_usuario = user_current.rol.value + nombre_usuario = user_current.nombre + print(nombre_usuario) + + if rol_usuario == "Cliente": + ordenes_de_trabajo = Orden_de_trabajo.query.filter_by( + usuario_id=id_propietario).all() + print(ordenes_de_trabajo) + else: + ordenes_de_trabajo = Orden_de_trabajo.query.filter_by( + mecanico_id=id_propietario).all() + print(ordenes_de_trabajo) + + ot_serialized_by_user = [] + + for orden_de_trabajo in ordenes_de_trabajo: + ot_serialized_by_user.append(orden_de_trabajo.serialize()) + + print(ot_serialized_by_user) + return jsonify({'msg': 'ok', 'ordenes_de_trabajo': ot_serialized_by_user}) + +# 🔹 REGISTRO DE USUARIO + + +@app.route('/register', methods=['POST']) +def register_user(): + body = request.get_json(silent=True) + if not body: + return jsonify({'msg': 'Debes enviar informacion de nuevo usuario en el body'}), 400 + + required_fields = ['nombre', 'identificacion', + 'password', 'telefono', 'email'] + for field in required_fields: + if field not in body: + return jsonify({'msg': f'Debes enviar el campo {field}'}), 400 + + new_user = User( + nombre=body['nombre'], + identificacion=body['identificacion'], + password=body['password'], + telefono=body['telefono'], + email=body['email'], + is_active=True, + rol=RolEnum.CLIENTE + ) + + db.session.add(new_user) + db.session.commit() + return jsonify({'msg': 'ok', 'user': new_user.serialize()}) + +# 🔹 LOGIN + + +@app.route('/login', methods=['POST']) +def login(): + body = request.get_json(silent=True) + if not body: + return jsonify({'msg': 'Debes enviar informacion en el body'}), 400 + + if 'email' not in body or 'password' not in body: + return jsonify({'msg': 'Email y password son obligatorios'}), 400 + + user = User.query.filter_by(email=body['email']).first() + if not user or user.password != body['password']: + return jsonify({'msg': 'Usuario o contraseña incorrectos'}), 400 + + tipo_de_usuario = user.rol.value + access_token = create_access_token( + identity=user.email, expires_delta=timedelta(hours=2)) + return jsonify({'msg': 'ok', 'token': access_token, 'tipo_de_usuario': tipo_de_usuario}), 200 + +# RECUPERAR CONTRASEÑA (SOLO UNA FUNCIÓN) + + +@app.route("/recuperar-password", methods=["POST"]) +def recuperar_password(): + data = request.get_json() + email = data.get("email") + + if not email: + return jsonify({"message": "El email es requerido"}), 400 + + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"message": "El correo no está registrado"}), 404 + + # GENERAR CÓDIGO DE 6 DÍGITOS + codigo = str(random.randint(100000, 999999)) + + # GUARDAR CÓDIGO EN MEMORIA + reset_codes[email] = codigo + print(f" Código generado para {email}: {codigo}") + + # ENVIAR CORREO + try: + msg = Message("Código de recuperación de contraseña", + recipients=[email]) + msg.body = f""" + Hola {user.nombre}, + + Tu código de recuperación es: {codigo} + + Ingresa este código en la web para restablecer tu contraseña. + """ + mail.send(msg) + + return jsonify({"message": "Se ha enviado un correo con tu código de recuperación"}), 200 + except Exception as e: + print("❌ Error enviando correo:", e) + return jsonify({"message": "Hubo un problema al enviar el correo"}), 500 + +# 🔹 VERIFICAR CÓDIGO + + +@app.route("/verificar-codigo", methods=["POST"]) +def verificar_codigo(): + data = request.get_json() + email = data.get("email") + codigo = data.get("codigo") + + if not email or not codigo: + return jsonify({"message": "Email y código son requeridos"}), 400 + + if email in reset_codes and reset_codes[email] == codigo: + verified_emails[email] = True + del reset_codes[email] + return jsonify({"message": "Código correcto. Ahora puedes restablecer tu contraseña."}), 200 + else: + return jsonify({"message": "Código incorrecto o expirado."}), 400 + +# 🔹 CAMBIAR CONTRASEÑA + + +@app.route("/resetPassword", methods=["POST"]) +def cambiar_password(): + data = request.get_json() + email = data.get("email") + codigo = data.get("codigo") + nueva_password = data.get("password") + + if not email or not codigo or not nueva_password: + return jsonify({"message": "Faltan datos"}), 400 + + if email not in verified_emails or not verified_emails[email]: + return jsonify({"message": "Primero debes verificar el código de recuperacón"}), 400 + + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"message": "Usuario no encontrado"}), 400 + + user.password = nueva_password # generate_password_hash(nueva_password) + db.session.commit() + + del verified_emails[email] + + return jsonify({"message": "Contraseña cambiada con éxito"}), 200 + -# this only runs if `$ python src/main.py` is executed +# ---------------------- MAIN ---------------------- if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) app.run(host='0.0.0.0', port=PORT, debug=True) diff --git a/src/front/assets/img/FB.svg b/src/front/assets/img/FB.svg new file mode 100644 index 0000000000..437a4e198e --- /dev/null +++ b/src/front/assets/img/FB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/IG.svg b/src/front/assets/img/IG.svg new file mode 100644 index 0000000000..dc669cf511 --- /dev/null +++ b/src/front/assets/img/IG.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/WA.svg b/src/front/assets/img/WA.svg new file mode 100644 index 0000000000..9f61ce3296 --- /dev/null +++ b/src/front/assets/img/WA.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/calidad.svg b/src/front/assets/img/calidad.svg new file mode 100644 index 0000000000..40a31e5cf5 --- /dev/null +++ b/src/front/assets/img/calidad.svg @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/front/assets/img/cumplimiento.svg b/src/front/assets/img/cumplimiento.svg new file mode 100644 index 0000000000..c3ed1ec4bc --- /dev/null +++ b/src/front/assets/img/cumplimiento.svg @@ -0,0 +1,2 @@ + + diff --git a/src/front/assets/img/experience.svg b/src/front/assets/img/experience.svg new file mode 100644 index 0000000000..59b64e1654 --- /dev/null +++ b/src/front/assets/img/experience.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/garantia.svg b/src/front/assets/img/garantia.svg new file mode 100644 index 0000000000..c036c80d20 --- /dev/null +++ b/src/front/assets/img/garantia.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/front/assets/img/img1.jpg b/src/front/assets/img/img1.jpg new file mode 100644 index 0000000000..247f99b1f0 Binary files /dev/null and b/src/front/assets/img/img1.jpg differ diff --git a/src/front/assets/img/img2.jpg b/src/front/assets/img/img2.jpg new file mode 100644 index 0000000000..35063215a7 Binary files /dev/null and b/src/front/assets/img/img2.jpg differ diff --git a/src/front/assets/img/img3.jpg b/src/front/assets/img/img3.jpg new file mode 100644 index 0000000000..a7bbb5f1aa Binary files /dev/null and b/src/front/assets/img/img3.jpg differ diff --git a/src/front/assets/img/logoAutoTekCeleste.svg b/src/front/assets/img/logoAutoTekCeleste.svg new file mode 100644 index 0000000000..aa82229b6e --- /dev/null +++ b/src/front/assets/img/logoAutoTekCeleste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/logoB.svg b/src/front/assets/img/logoB.svg new file mode 100644 index 0000000000..5caac22aff --- /dev/null +++ b/src/front/assets/img/logoB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/profesionalismo.svg b/src/front/assets/img/profesionalismo.svg new file mode 100644 index 0000000000..00c36fc36d --- /dev/null +++ b/src/front/assets/img/profesionalismo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/assets/img/resetmecanico.jpg b/src/front/assets/img/resetmecanico.jpg new file mode 100644 index 0000000000..8f3f40560f Binary files /dev/null and b/src/front/assets/img/resetmecanico.jpg differ diff --git a/src/front/assets/img/servicio1.jpg b/src/front/assets/img/servicio1.jpg new file mode 100644 index 0000000000..005e2eae75 Binary files /dev/null and b/src/front/assets/img/servicio1.jpg differ diff --git a/src/front/assets/img/servicio2.jpg b/src/front/assets/img/servicio2.jpg new file mode 100644 index 0000000000..b106cd3dd0 Binary files /dev/null and b/src/front/assets/img/servicio2.jpg differ diff --git a/src/front/assets/img/servicio3.jpg b/src/front/assets/img/servicio3.jpg new file mode 100644 index 0000000000..30a3e79934 Binary files /dev/null and b/src/front/assets/img/servicio3.jpg differ diff --git a/src/front/assets/img/wallet.svg b/src/front/assets/img/wallet.svg new file mode 100644 index 0000000000..25350db16c --- /dev/null +++ b/src/front/assets/img/wallet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/front/components/Carousel.jsx b/src/front/components/Carousel.jsx new file mode 100644 index 0000000000..b4c9f492de --- /dev/null +++ b/src/front/components/Carousel.jsx @@ -0,0 +1,46 @@ +import { Link } from "react-router-dom"; +import img1 from "../assets/img/img1.jpg"; +import img2 from "../assets/img/img2.jpg"; +import img3 from "../assets/img/img3.jpg"; + + +export const Carousel = () => { + return ( +
+
+
+ img1 +
+
+ img2 +
+
+ img3 +
+
+ + + +
+ ); +}; diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..0364ee08ad 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,36 @@ +import logoB from "../assets/img/logoB.svg"; + + export const Footer = () => ( - +
+
+
+ + +
+
+ Logo +
+
+ + +
+
Ubicación
+

+ Avenida Única, calle 10B este Parque Industrial, edificio 5011
+ Ciudad del Saber +

+
+ + +
+
Horarios
+

+ Lun a Vier: 8:00 am - 6:00 pm
+ Sábados: 10:00 am - 4:00 pm +

+
+
+
+
); diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..ff8086cf6f 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,47 @@ import { Link } from "react-router-dom"; +import { HashLink } from 'react-router-hash-link'; //se debd instalar - npm install react-router-hash-link +import logoB from "../assets/img/logoB.svg"; + export const Navbar = () => { + return ( + + ); +}; diff --git a/src/front/components/NavbarMecanico.jsx b/src/front/components/NavbarMecanico.jsx new file mode 100644 index 0000000000..f6e98c1b03 --- /dev/null +++ b/src/front/components/NavbarMecanico.jsx @@ -0,0 +1,50 @@ +import { Link, useNavigate } from "react-router-dom"; +import logoB from "../assets/img/logoB.svg"; + + +export const NavbarMecanico = () => { + + + const navigate = useNavigate() + + const handleLogout = () => { + localStorage.removeItem("jwt_token"); + //setIsLogged(false); + navigate("/"); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/front/components/NavbarUser.jsx b/src/front/components/NavbarUser.jsx new file mode 100644 index 0000000000..22b2eb240c --- /dev/null +++ b/src/front/components/NavbarUser.jsx @@ -0,0 +1,50 @@ +import { Link, useNavigate } from "react-router-dom"; +import logoB from "../assets/img/logoB.svg"; + +export const NavbarUser = () => { + + const navigate = useNavigate() + + const handleLogout = () => { + localStorage.removeItem("jwt_token"); + //setIsLogged(false); + navigate("/"); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/front/components/Servicios.jsx b/src/front/components/Servicios.jsx new file mode 100644 index 0000000000..cd524c4e2a --- /dev/null +++ b/src/front/components/Servicios.jsx @@ -0,0 +1,184 @@ +import { Link } from "react-router-dom"; +import servicio1 from "../assets/img/servicio1.jpg"; +import servicio2 from "../assets/img/servicio2.jpg"; +import servicio3 from "../assets/img/servicio3.jpg"; +import experience from "../assets/img/experience.svg"; +import calidad from "../assets/img/calidad.svg"; +import wallet from "../assets/img/wallet.svg"; +import cumplimiento from "../assets/img/cumplimiento.svg"; +import profesionalismo from "../assets/img/profesionalismo.svg"; +import garantia from "../assets/img/garantia.svg"; +import IG from "../assets/img/IG.svg"; +import WA from "../assets/img/WA.svg"; +import FB from "../assets/img/FB.svg"; + + + + + +export const Servicios = () => { + const infoCards = [ + { + titulo: "Del taller a tu pantalla: Transparencia en cada etapa.", + texto: "Sabemos lo importante que es tu tiempo y la confianza que depositas en nosotros. Por eso, en AutoTekc, te ofrecemos un proceso completamente transparente. Mira cómo avanzan los servicios de tu auto, revisa tu historial y gestiona tus vehículos registrados con total comodidad. Olvídate de las esperas y dudas." + }, + { + titulo: "Expertos a tu servicio, tu ritmo es lo primero!", + texto: "¿Necesitas un diagnóstico o ya sabes qué servicio requiere tu auto? Nuestro equipo de especialistas está listo para asesorarte, o si lo prefieres, elige los servicios que desees con la facilidad de unos pocos clics. Eficiencia, calidad y control, directamente en tus manos." + }, + { + titulo: "Tu taller, tus reglas: Control total sobre tu vehículo.", + texto: " Imagina tener el control total sobre el cuidado de tu auto. En Autotekc, lo hicimos posible. Desde agendar tu cita y seleccionar los servicios que necesitas, hasta seguir el proceso de tu vehículo en tiempo real y revisar tu historial, todo está al alcance de tu mano. Nos enfocamos en darte la confianza y la claridad que mereces, eliminando las sorpresas y optimizando tu tiempo." + } + ]; + + const servicios = [ + { titulo: "Cambio de aceite", imagen: servicio1 }, + { titulo: "Alineación y balanceo", imagen: servicio2 }, + { titulo: "Diagnóstico computarizado", imagen: servicio3 } + ]; + + return ( + <> +
+

¿Qué Ofrece AutoTekc?

+ +
+ {infoCards.map((item, i) => ( +
+
+
+
{item.titulo}
+

{item.texto}

+
+
+
+
+ ))} +
+
+ + +
+
+ {servicios.map((servicio, i) => ( +
+
+
+ {servicio.titulo} +
+
{servicio.titulo}
+
+
+
+
+ ))} +
+ +
+ + ¡Agenda tu cita aquí! + +
+ + +
+
+

Visión

+

+ En AutoTekc, nacimos de una visión clara y una necesidad palpable: transformar la experiencia de llevar tu auto al servicio. + Cansados de la opacidad, los procesos inciertos y la sensación de desconexión que a menudo acompaña la mecánica automotriz, + decidimos crear un espacio donde la confianza, la transparencia y la agilidad fueran los pilares fundamentales. + Entendemos que tu auto es más que un medio de transporte; es una extensión de tu vida, + y por eso, nuestra idea germinó de la inquietud de ofrecerte la tranquilidad de saber exactamente qué sucede con él, en todo momento. +

+
+ +
+
+ experience +
Experiencia
+

Va casi 20 años cuidando tu auto.

+
+ +
+ calidad +
Calidad
+

Productos de máxima calidad, pintura de fábrica e insumos óptimos.

+
+ +
+ wallet +
Precio Justo
+

La calidad tiene un precio, igualmente tiene la satisfacción garantizada.

+
+ +
+ cumplimiento +
Cumplimiento
+

El tiempo de nuestros clientes es valioso, por eso el cumplimiento es importante para nosotros.

+
+ +
+ profesionalismo +
Profesionalismo
+

Personal profesional, actualizado y altamente capacitado.

+
+ +
+ garantia +
Garantía
+

Ofrecemos garantía de por vida en nuestro trabajo.

+
+
+
+
+ +
+
+
+

Contactanos

+

Teléfonos: +00 100-00018 / +00 100-00019

+

Correo: online@autotek.com

+ +
+ IG + WA + FB +
+
+ +
+
+ + + + + +
+
+
+
+ + ); + +}; + diff --git a/src/front/components/VehicleCard.jsx b/src/front/components/VehicleCard.jsx new file mode 100644 index 0000000000..459f0c9e2b --- /dev/null +++ b/src/front/components/VehicleCard.jsx @@ -0,0 +1,75 @@ +import React from 'react' +import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import { Link, useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; + + + +export const VehicleCard = (props) => { + + + const navigate = useNavigate() + function eliminarVehiculo(id_vehiculo) { + + const token = localStorage.getItem("jwt_token") + fetch(import.meta.env.VITE_BACKEND_URL + `eliminar_vehiculo/${id_vehiculo}`, { + method: "DELETE", + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token //localStorage.getItem('token') // JWT + } + }) + .then(res => res.json()) + .then(data => { + console.log(data) + if (data.msg === 'Vehículo eliminado correctamente') { + //alert('Vehículo eliminado con éxito') + console.log("Navegando a /vehiculos...") + props.onDelete(); + //navigate('/vehiculos') //window.location.href = '/vehiculos' + console.log("despues de navigate") + } else { + alert(data.msg) + } + }) + .catch(error => { + console.error('Error:', error) + //alert('Hubo un error al intentar eliminar el vehículo') + }) + } + return ( +
+
+
+
+ ... +
+
+
+

Matricula {props.matricula}

+
Marca: {props.marca}
+
Modelo: {props.modelo}
+
Año: {props.year}
+
+
+
+ + + + + + +
+
+
+ +
+ + + + ) +} + diff --git a/src/front/index.css b/src/front/index.css index e69de29bb2..de868172bf 100644 --- a/src/front/index.css +++ b/src/front/index.css @@ -0,0 +1,51 @@ + +.modal-colors { + + background-color: #007bff; +} + +.modal-colors:hover { + background-color: #16385d; /* un color más oscuro al hacer hover */ + color: white; +} + +.servicio-card { + position: relative; + border-radius: 12px; + overflow: hidden; +} + +.servicio-card img { + transition: transform 0.4s ease; +} + +.servicio-card:hover img { + transform: scale(1.1); +} + +.overlay { + background: rgba(0, 43, 91, 0.7); + opacity: 0; + transition: opacity 0.4s ease; +} + +.servicio-card:hover .overlay { + opacity: 1; +} + +.vision-box { + background-color: #dceeff; + padding: 30px; + border-radius: 20px; + margin-bottom: 40px; +} + +.icon-feature { + text-align: center; + padding: 20px; +} + +.icon-feature i { + font-size: 2rem; + color: #0d6efd; +} diff --git a/src/front/login.css b/src/front/login.css new file mode 100644 index 0000000000..a40324a3e7 --- /dev/null +++ b/src/front/login.css @@ -0,0 +1,159 @@ +.left-panel { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 90vh; + padding: 2rem; + text-align: center; +} + +.login-card { + background-color: #214F84; + border-radius: 1rem; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + padding: 2.5rem; + min-height: 90vh; + color: #fff; + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; +} + +.login-card .input-group { + position: relative; + margin-bottom: 1rem; +} + +.login-card .input-group-text { + background-color: transparent; + border: none; + color: #ccc; + position: absolute; + top: 50%; + left: 0.75rem; + transform: translateY(-50%); + z-index: 2; + pointer-events: none; + align-items: center; +} + +.login-card .form-control { + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.5); + color: #333; + border-radius: 0.5rem !important; + padding: 0.75rem 1rem; + position: relative; + z-index: 1; +} + +.login-card .form-control::placeholder { + color: #888; +} + +.login-card .btn-primary { + background-color: #007bff; + border-color: #007bff; + border-radius: 0.5rem; + padding: 0.75rem 1.5rem; + font-weight: bold; +} + +.login-card .btn-primary:hover { + background-color: #2a74c4; + border-color: #2a74c4; +} + +.login-card a { + color: #fff; + text-decoration: none; +} + +.login-card a:hover { + text-decoration: underline; +} + +.back-link { + top: 30px; + left: 40px; + color: #6c757d; + font-weight: bold; + text-decoration: none; +} + +.back-link i { + margin-right: 5px; +} + +.register-section { + background-color: #1e3859; + padding: 1.25rem 2.5rem; + text-align: center; + margin: 0 -2.5rem -2.5rem -2.5rem; +} + +.logo-text { + color: #31649f; + font-weight: bold; +} + +.welcome-text { + color: #fff; + margin-bottom: 2rem; +} + +.login-page-container { + width: 100%; + min-height: 100vh; + background-color: #f8f9fa; + font-family: Arial, sans-serif; + display: flex; + overflow-y: hidden; + overflow-x: hidden; +} + +.input-wrapper { + position: relative; + margin-bottom: 1rem; +} + +.input-wrapper .password-input { + width: 100%; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.5); + color: #333; + border-radius: 0.5rem !important; + padding: 0.75rem 1rem; + padding-left: 3rem; + padding-right: 3rem; + position: relative; + z-index: 1; +} + + +.input-wrapper .fas { + position: absolute; + top: 50%; + transform: translateY(-50%); + color: #868686; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 2.5rem; +} + +.input-wrapper .password-icon-left { + left: 0; + pointer-events: none; +} + +.input-wrapper .password-icon-right { + right: 0; + cursor: pointer; + padding-right: 0.75rem; + padding-left: 0.75rem; +} \ No newline at end of file diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 341ed21768..c1e2c5c5b0 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -1,52 +1,18 @@ -import React, { useEffect } from "react" -import rigoImageUrl from "../assets/img/rigo-baby.jpg"; -import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; +import React from "react"; +import { Navbar } from "../components/Navbar"; +import { Carousel } from "../components/Carousel"; +import { Servicios } from "../components/Servicios"; +import { Footer } from "../components/Footer"; export const Home = () => { - - const { store, dispatch } = useGlobalReducer() - - const loadMessage = async () => { - try { - const backendUrl = import.meta.env.VITE_BACKEND_URL - - if (!backendUrl) throw new Error("VITE_BACKEND_URL is not defined in .env file") - - const response = await fetch(backendUrl + "/api/hello") - const data = await response.json() - - if (response.ok) dispatch({ type: "set_hello", payload: data.message }) - - return data - - } catch (error) { - if (error.message) throw new Error( - `Could not fetch the message from the backend. - Please check if the backend is running and the backend port is public.` - ); - } - - } - - useEffect(() => { - loadMessage() - }, []) - - return ( -
-

Hello Rigo!!

-

- Rigo Baby -

-
- {store.message ? ( - {store.message} - ) : ( - - Loading message from the backend (make sure your python 🐍 backend is running)... - - )} -
-
- ); -}; \ No newline at end of file + return ( +
+ + +
+ +
+
+
+ ); +}; diff --git a/src/front/pages/InicioMecanico.jsx b/src/front/pages/InicioMecanico.jsx new file mode 100644 index 0000000000..b572b354e8 --- /dev/null +++ b/src/front/pages/InicioMecanico.jsx @@ -0,0 +1,8 @@ +import React from "react"; +import { NavbarMecanico } from "../components/NavbarMecanico"; + +export const InicioMecanico = () => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/front/pages/InicioUser.jsx b/src/front/pages/InicioUser.jsx new file mode 100644 index 0000000000..196f64e15e --- /dev/null +++ b/src/front/pages/InicioUser.jsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; +import { NavbarUser } from "../components/NavbarUser"; + + +export const InicioUser = () => { + + const [ordenDeTrabajo, setOrdenDeTrabajo] = useState([]) + + function traer_ordenes_de_servicio(){ + + const token = localStorage.getItem("jwt_token") + fetch(import.meta.env.VITE_BACKEND_URL + "ordenes_de_trabajo", { + method: "GET", + headers: { + "Content-Type": "application/json", + "authorization": 'Bearer ' + token + } + }) + .then((response)=>{ + if(!response.ok) alert("Error al traer la informacion") + return response.json() + }) + .then((data)=>{ + console.log(data.ordenes_de_trabajo) + setOrdenDeTrabajo(data.ordenes_de_trabajo) + }) + .catch((error)=>{error}) + } + + useEffect(() => { + traer_ordenes_de_servicio() + }, []) + + const getEstadoBadge = (estado) => { + if (estado === 'En Proceso') { + return En Proceso; + } + else if (estado == 'Ingresado'){ + return Ingresado; + } + else + return Finalizado; + }; + + return ( +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + + + {ordenDeTrabajo.map((orden) => ( + + + + + + + + + + ))} + +
Nro. de ÓrdenVehículoMecanicoServiciosFecha de ingresoFecha de salidaEstado
{orden.id_ot}{orden.matricula_vehiculo}{orden.nombre_mecanico}{orden.servicios_asociados.map(s => s.servicio.name_service).join(", ")}{orden.fecha_ingreso}{orden.fecha_final}{getEstadoBadge(orden.estado_servicio)}
+
+
+ +
+ ); +}; \ No newline at end of file diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 9bfa31325c..0da6a30d64 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -7,9 +7,7 @@ import { Footer } from "../components/Footer" export const Layout = () => { return ( - -