From be6e6d1153d84f155ac0b12b7743bc16db5a00bc Mon Sep 17 00:00:00 2001 From: Nika254 Date: Thu, 6 Nov 2025 14:15:06 +0000 Subject: [PATCH 01/54] first --- src/api/routes.py | 104 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..b984a8d032 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,106 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" -from flask import Flask, request, jsonify, url_for, Blueprint +from flask import request, jsonify, Blueprint +import secrets +from datetime import datetime, timedelta, timezone +import jwt +from werkzeug.security import check_password_hash from api.models import db, User -from api.utils import generate_sitemap, APIException +from werkzeug.security import generate_password_hash from flask_cors import CORS -api = Blueprint('api', __name__) -# Allow CORS requests to this API +def token_requerido(f): + def wrapper(*args, **kwargs): + auth = request.headers.get('Authorization') + if not auth or not auth.startswith('Bearer '): + return jsonify({"msg": "Token requerido"}), 401 + token = auth.split(' ')[1] + try: + jwt.decode(token, SECRET_KEY, algorithms=['HS256']) + except Exception: + return jsonify({"msg": "Token inválido"}), 401 + return f(*args, **kwargs) + wrapper.__name__ = f.__name__ + return wrapper + + +SECRET_KEY = "super-secret-key" + +api = Blueprint('api', __name__) CORS(api) +reset_tokens = {} + + +@api.route('/forgot-password', methods=['POST']) +def forgot_password(): + data = request.get_json() + email = data.get('email') + if not email: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"msg": "Usuario no encontrado o no registrado"}), 404 + token = secrets.token_urlsafe(16) + reset_tokens[email] = token + return jsonify({"msg": "Token de recuperacion", "token": token}), 200 + + +@api.route('/reset-password', methods=['POST']) +def reset_password(): + data = request.get_json() + email = data.get('email') + token = data.get('token') + new_password = data.get('new_password') + if not email or not token or not new_password: + return jsonify({"msg": "Faltan datos"}), 400 + if reset_tokens.get(email) != token: + return jsonify({"msg": "Token inválido"}), 401 + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({"msg": "Usuario no encontrado"}), 404 + user.password = generate_password_hash(new_password) + db.session.commit() + reset_tokens.pop(email) + return jsonify({"msg": "Cambio de contraseña"}), 200 + + +@api.route('/login', methods=['POST']) +def login(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + if not email or not password: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + user = User.query.filter_by(email=email).first() + if not user or not check_password_hash(user.password, password): + return jsonify({"msg": "Usuario o contraseña incorrectos"}), 401 + token = jwt.encode({ + 'user_id': user.id, + 'exp': datetime.now(timezone.utc) + timedelta(minutes=15) + }, SECRET_KEY, algorithm="HS256") + return jsonify({"token": token}) + + +@api.route('/register', methods=['POST']) +def register(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + if not email or not password: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + if User.query.filter_by(email=email).first(): + return jsonify({"msg": "El usuario ya existe"}), 400 + hashed_password = generate_password_hash(password) + new_user = User(email=email, password=hashed_password, is_active=True) + db.session.add(new_user) + db.session.commit() + return jsonify({"msg": "Usuario registrado exitosamente"}), 201 + @api.route('/hello', methods=['POST', 'GET']) +@token_requerido def handle_hello(): - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" + "message": "¡Hola! Acceso autorizado al backend." } - return jsonify(response_body), 200 From a77734248cb95c0d96609402f2f51f99b7a8c138 Mon Sep 17 00:00:00 2001 From: Mnrubn <126007823+Mnrubn@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:36:45 +0000 Subject: [PATCH 02/54] commit --- package-lock.json | 56 ++++++++++--------------- src/front/components/Footer.jsx | 8 +--- src/front/components/Form.jsx | 74 +++++++++++++++++++++++++++++++++ src/front/index.css | 13 ++++++ src/front/pages/Login.jsx | 43 +++++++++++++++++++ src/front/pages/Register.jsx | 39 +++++++++++++++++ src/front/routes.jsx | 4 ++ 7 files changed, 196 insertions(+), 41 deletions(-) create mode 100644 src/front/components/Form.jsx create mode 100644 src/front/pages/Login.jsx create mode 100644 src/front/pages/Register.jsx diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..44f1ece2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -884,7 +885,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -997,14 +997,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1018,6 +1010,7 @@ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1066,6 +1059,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1294,6 +1288,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1312,8 +1307,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -1797,6 +1791,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3486,6 +3481,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3498,6 +3494,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3921,7 +3918,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3933,7 +3929,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4081,7 +4076,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4100,8 +4094,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/text-table": { "version": "0.2.0", @@ -4278,6 +4271,7 @@ "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4500,6 +4494,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4950,7 +4945,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -5044,14 +5038,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5063,6 +5049,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5098,7 +5085,8 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -5245,6 +5233,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5257,8 +5246,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "call-bind": { "version": "1.0.8", @@ -5600,6 +5588,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6702,6 +6691,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -6710,6 +6700,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6988,7 +6979,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6999,8 +6989,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7100,7 +7089,6 @@ "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -7113,8 +7101,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7228,6 +7215,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..8361fa8a93 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,5 @@ export const Footer = () => ( ); diff --git a/src/front/components/Form.jsx b/src/front/components/Form.jsx new file mode 100644 index 0000000000..a9b2241ea4 --- /dev/null +++ b/src/front/components/Form.jsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from "react"; + +const Form = ({ mode, onSubmit, successMessage }) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [errorMsn, setErrorMsn] = useState(null); + + useEffect(() => { + if (successMessage) { + setErrorMsn(successMessage); + } + }, [successMessage]); + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + await onSubmit({ email, password, setErrorMsn }); + setEmail(""); + setPassword(""); + } catch (error) { + setErrorMsn(error.message || "Error"); + console.error(error.message || "Error"); + } + }; + + return ( +
+
+
+

+ {mode === "register" ? "Registrarse" : "Iniciar Sesión"} +

+
+
+ + setEmail(e.target.value)} + required + /> +
+ +
+ + setPassword(e.target.value)} + required + /> +
+ + {errorMsn && ( +
+ {errorMsn} +
+ )} + + +
+
+
+
+ ); +}; + +export default Form; \ No newline at end of file diff --git a/src/front/index.css b/src/front/index.css index e69de29bb2..bd8850d3a3 100644 --- a/src/front/index.css +++ b/src/front/index.css @@ -0,0 +1,13 @@ +body { + min-height: 100vh; + margin: 0; + background: linear-gradient(180deg, #1e91ed 10%, #7f00b2 90%); + background-repeat: no-repeat; + background-attachment: fixed; +} + +.button{ + background-color:#1e91ed; + color: white; + +} diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx new file mode 100644 index 0000000000..b447e1aab1 --- /dev/null +++ b/src/front/pages/Login.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import Form from "../components/Form.jsx"; + +const Login = () => { + const navigate = useNavigate(); + const location = useLocation(); + const successMessage = location.state?.successMessage; + + const handleLogin = async ({ email, password, setErrorMsn }) => { + setErrorMsn(null); + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + try { + const response = await fetch(`${backendUrl}/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + setErrorMsn(data.msg || "Error en el inicio de sesión"); + console.error(data.msg || "Error en el inicio de sesión"); + return; + } + + localStorage.setItem("token", data.token); + + navigate("/private", { + state: { successMessage: "Inicio de sesión exitoso" }, + }); + } catch (error) { + setErrorMsn("Error al conectar con el servidor"); + console.error("Error al conectar con el servidor", error); + } + }; + + return
; +}; + +export default Login; \ No newline at end of file diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx new file mode 100644 index 0000000000..9ea28f2b55 --- /dev/null +++ b/src/front/pages/Register.jsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import Form from "../components/Form.jsx"; + +const Register = () => { + const navigate = useNavigate(); + + const handleSignup = async ({ email, password, setErrorMsn }) => { + setErrorMsn(null); + const backendUrl = import.meta.env.VITE_BACKEND_URL; + + try { + const response = await fetch(`${backendUrl}/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (!response.ok) { + setErrorMsn(data.msg || "Error en el registro"); + console.error(data.msg || "Error en el registro"); + return; + } + + navigate("/login", { + state: { successMessage: "Registro exitoso. Ahora puedes iniciar sesión." }, + }); + } catch (error) { + setErrorMsn("Error al conectar con el servidor"); + console.error("Error al conectar con el servidor", error); + } + }; + + return ; +}; + +export default Register; \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index 0557df6141..02ff2c3aad 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -9,6 +9,8 @@ import { Layout } from "./pages/Layout"; import { Home } from "./pages/Home"; import { Single } from "./pages/Single"; import { Demo } from "./pages/Demo"; +import Register from "./pages/Register"; +import Login from "./pages/Login"; export const router = createBrowserRouter( createRoutesFromElements( @@ -25,6 +27,8 @@ export const router = createBrowserRouter( } /> } /> {/* Dynamic route for single items */} } /> + } /> + } /> ) ); \ No newline at end of file From 66a8e3a3b6551640dbe2eab6af3bbbcff2f19882 Mon Sep 17 00:00:00 2001 From: Nika254 Date: Fri, 7 Nov 2025 19:46:10 +0000 Subject: [PATCH 03/54] commit Abel --- README.md | 6 ++--- package-lock.json | 56 +++++++++++++++++++---------------------------- src/api/admin.py | 8 +++---- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 6b782b220d..63d4ff2e7b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Build web applications using React.js for the front end and python/flask for you ### 1) Installation: -> If you use Github Codespaces (recommended) or Gitpod this template will already come with Python, Node and the Posgres Database installed. If you are working locally make sure to install Python 3.10, Node +> If you use Github Codespaces (recommended) or Gitpod this template will already come with Python, Node and the Posgres Database installed. If you are working locally make sure to install Python 3.10, Node It is recomended to install the backend first, make sure you have Python 3.10, Pipenv and a database engine (Posgress recomended) @@ -61,11 +61,11 @@ And you will see the following message: ### **Important note for the database and the data inside it** -Every Github codespace environment will have **its own database**, so if you're working with more people eveyone will have a different database and different records inside it. This data **will be lost**, so don't spend too much time manually creating records for testing, instead, you can automate adding records to your database by editing ```commands.py``` file inside ```/src/api``` folder. Edit line 32 function ```insert_test_data``` to insert the data according to your model (use the function ```insert_test_users``` above as an example). Then, all you need to do is run ```pipenv run insert-test-data```. +Every Github codespace environment will have **its own database**, so if you're working with more people eveyone will have a different database and different records inside it. This data **will be lost**, so don't spend too much time manually creating records for testing, instead, you can automate adding records to your database by editing `commands.py` file inside `/src/api` folder. Edit line 32 function `insert_test_data` to insert the data according to your model (use the function `insert_test_users` above as an example). Then, all you need to do is run `pipenv run insert-test-data`. ### Front-End Manual Installation: -- Make sure you are using node version 20 and that you have already successfully installed and runned the backend. +- Make sure you are using node version 20 and that you have already successfully installed and runned the backend. 1. Install the packages: `$ npm install` 2. Start coding! start the webpack dev server `$ npm run start` diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..44f1ece2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -884,7 +885,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -997,14 +997,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1018,6 +1010,7 @@ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1066,6 +1059,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1294,6 +1288,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1312,8 +1307,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -1797,6 +1791,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3486,6 +3481,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3498,6 +3494,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3921,7 +3918,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3933,7 +3929,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4081,7 +4076,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4100,8 +4094,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/text-table": { "version": "0.2.0", @@ -4278,6 +4271,7 @@ "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4500,6 +4494,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4950,7 +4945,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -5044,14 +5038,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5063,6 +5049,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5098,7 +5085,8 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -5245,6 +5233,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5257,8 +5246,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "call-bind": { "version": "1.0.8", @@ -5600,6 +5588,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6702,6 +6691,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -6710,6 +6700,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6988,7 +6979,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6999,8 +6989,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7100,7 +7089,6 @@ "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -7113,8 +7101,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7228,6 +7215,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", diff --git a/src/api/admin.py b/src/api/admin.py index 3eecb64140..b054170fc8 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,17 +1,17 @@ - + import os from flask_admin import Admin from .models import db, User from flask_admin.contrib.sqla import ModelView + 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='4Geeks Admin') - # 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)) # 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)) From b75a66726f5a1aeedde0d87380ceb36c1de26a5f Mon Sep 17 00:00:00 2001 From: JorgeCKR Date: Fri, 7 Nov 2025 20:07:10 +0000 Subject: [PATCH 04/54] Commit Jorge --- package-lock.json | 56 +++++++++++++-------------------- src/front/components/Navbar.jsx | 7 +++-- src/front/styles/navbar.css | 4 +++ 3 files changed, 31 insertions(+), 36 deletions(-) create mode 100644 src/front/styles/navbar.css diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..44f1ece2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -884,7 +885,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -997,14 +997,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1018,6 +1010,7 @@ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1066,6 +1059,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1294,6 +1288,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1312,8 +1307,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -1797,6 +1791,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3486,6 +3481,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3498,6 +3494,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3921,7 +3918,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3933,7 +3929,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4081,7 +4076,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4100,8 +4094,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "node_modules/text-table": { "version": "0.2.0", @@ -4278,6 +4271,7 @@ "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", @@ -4500,6 +4494,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4950,7 +4945,6 @@ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -5044,14 +5038,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5063,6 +5049,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "dev": true, + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5098,7 +5085,8 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -5245,6 +5233,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -5257,8 +5246,7 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, - "optional": true, - "peer": true + "optional": true }, "call-bind": { "version": "1.0.8", @@ -5600,6 +5588,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6702,6 +6691,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -6710,6 +6700,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6988,7 +6979,6 @@ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, - "peer": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -6999,8 +6989,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7100,7 +7089,6 @@ "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", "dev": true, "optional": true, - "peer": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -7113,8 +7101,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, - "optional": true, - "peer": true + "optional": true } } }, @@ -7228,6 +7215,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.18.10", "fsevents": "~2.3.2", diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..b5dff92514 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -6,11 +6,14 @@ export const Navbar = () => { - {/* MAIN CONTENT */} + {/* --- CONTENIDO PRINCIPAL --- */}
-
-
- - -
-
+
@@ -137,18 +223,15 @@ export const Dashboard = () => {
- - {/* TAREAS PERSONALES */} + {/* Tus Tareas Pendientes */}

Tus Tareas Pendientes

-
-
    {pendingUserTasks.length > 0 ? ( pendingUserTasks.map(task => ( @@ -166,49 +249,43 @@ export const Dashboard = () => {
- {/* FINANZAS */} + {/* --- TARJETA DE FINANZAS UNIFICADA --- */}

Resumen Financiero

+ {/* Saldo Bote Personal */}

Saldo del Bote

-

- {store.personalBote.toFixed(2)}€ -

+

{store.personalBote.toFixed(2)}€

- + {/* Gastos del Mes (Total) */}

Gastos del Mes

-

- {totalExpenses.toFixed(2)}€ -

+

{totalExpenses.toFixed(2)}€

+ {/* --- FIN DE TARJETA UNIFICADA --- */} - {/* TAREAS CLAN */} + {/* Tareas de Clanes */}

Tareas de Clanes

-
- - {activeClan && -

- Para: {activeClan.name} -

} + {/* --- SUBTÍTULO AÑADIDO --- */} + {activeClan &&

Para: {activeClan.name}

}
    {activeClanTasks.length > 0 ? ( @@ -221,21 +298,18 @@ export const Dashboard = () => { /> )) ) : ( -

    - No hay tareas de clan. -

    +

    No hay tareas de clan.

    )}
- {/* RESUMEN COMPLETADAS */} + {/* Tareas Completadas */}

Tareas Completadas (Total)

-

+

{completedTaskCount}

¡Sigue así!

@@ -243,16 +317,21 @@ export const Dashboard = () => {
- {/* MENSAJES */} + {/* Próximos Eventos */}
+
+

Próximos Eventos

+

No hay eventos próximos.

+
+
+ + {/* Mensajes (Placeholder) */} +

Mensajes

-

- -

+

-
diff --git a/src/front/pages/Finances.jsx b/src/front/pages/Finances.jsx index e058645705..7f7cd01e84 100644 --- a/src/front/pages/Finances.jsx +++ b/src/front/pages/Finances.jsx @@ -3,202 +3,239 @@ import useGlobalReducer from "../hooks/useGlobalReducer"; import "../styles/ProfileGroups.css"; export const Finances = () => { - const { store } = useGlobalReducer(); + const { store, dispatch } = useGlobalReducer(); const activeClanId = store.activeClanId; - // Bote Personal - const personalBote = store.personalBote ?? 0.00; - - // Por ahora NO tenemos estos en el store — ponemos placeholder seguro - const personalExpenses = []; - const activeExpenses = []; - const activeBalances = []; - const currentBote = 0.00; + // Estados para Gasto de Clan + const [clanConcept, setClanConcept] = useState(""); + const [clanAmount, setClanAmount] = useState(""); - // Estados formularios + // --- NUEVO: Estados para Gasto Personal --- const [personalConcept, setPersonalConcept] = useState(""); const [personalAmount, setPersonalAmount] = useState(""); - const [clanConcept, setClanConcept] = useState(""); - const [clanAmount, setClanAmount] = useState(""); - + // Estados para Modales const [showBoteModal, setShowBoteModal] = useState(false); - const [boteAmount, setBoteAmount] = useState(""); + const [boteAmount, setBoteAmount] = useState(""); // Cambiado a string para placeholder + + // --- FILTRADO DE DATOS --- + const currentBote = store.commonBote[activeClanId] || 0.00; + const activeExpenses = store.expenses.filter(exp => exp.clanId === activeClanId); + const activeBalances = store.balances.filter(bal => bal.clanId === activeClanId); + // --- NUEVO: Datos personales --- + const personalBote = store.personalBote; + const personalExpenses = store.personalExpenses; + + // --- HANDLERS --- + const handleAddClanExpense = (e) => { + e.preventDefault(); + if (!clanConcept || !clanAmount || parseFloat(clanAmount) <= 0) return alert("Introduce un concepto y un importe válido."); + dispatch({ type: "ADD_EXPENSE", payload: { concept: clanConcept, amount: clanAmount } }); + setClanConcept(""); + setClanAmount(""); + }; + + // --- NUEVO: Handler para Gasto Personal --- + const handleAddPersonalExpense = (e) => { + e.preventDefault(); + if (!personalConcept || !personalAmount || parseFloat(personalAmount) <= 0) return alert("Introduce un concepto y un importe válido."); + dispatch({ type: "ADD_PERSONAL_EXPENSE", payload: { concept: personalConcept, amount: personalAmount } }); + setPersonalConcept(""); + setPersonalAmount(""); + }; + + const handleAddToBote = (e) => { + e.preventDefault(); + if (parseFloat(boteAmount) <= 0 || !boteAmount) return alert("Introduce un importe positivo."); + dispatch({ type: "ADD_TO_BOTE", payload: { amount: boteAmount } }); + setShowBoteModal(false); + setBoteAmount(""); + }; return (
- - {/* MODAL PARA BOTE DEL CLAN */} + {/* --- MODAL PARA AÑADIR AL BOTE (Clan) --- */} {showBoteModal && ( - <> -
-
-
-
e.preventDefault()}> -
-
Añadir fondos al Bote Común
- -
-
-
- - setBoteAmount(e.target.value)} - placeholder="0" - /> -
-
-
- - +
+
+
+ +
+
Añadir fondos al Bote Común
+ +
+
+
+ + setBoteAmount(e.target.value)} + placeholder="0" + />
- -
+
+
+ + +
+
- -
- +
)} + {showBoteModal &&
} - {/* FINANZAS PERSONALES */} + {/* --- INICIO: SECCIÓN FINANZAS PERSONALES --- */}

Tus Finanzas Personales

-
+ {/* Bote Personal */}
-
+

Bote Personal

-

- {personalBote.toFixed(2)} € -

+

{personalBote.toFixed(2)} €

- + {/* Añadir Gasto Personal */}
-
+

Añadir Gasto Personal

- -
e.preventDefault()} className="mt-3"> +
- - setPersonalConcept(e.target.value)} - placeholder="Ej: Café" /> + + setPersonalConcept(e.target.value)} placeholder="Ej: Café" />
-
- - Importe (€) + setPersonalAmount(e.target.value)} - placeholder="0" /> + placeholder="0" + /> +
+
+
- -
- + {/* Historial Gastos Personales */}

Historial de Gastos Personales

-

- Función pendiente de restaurar 🔧 -

+
    + {personalExpenses.length > 0 ? personalExpenses.map(expense => ( +
  • +
    + {expense.concept} + Fecha: {expense.date} +
    + -{expense.amount.toFixed(2)} € +
  • + )) : ( +

    No hay gastos personales.

    + )} +
+ {/* --- FIN: SECCIÓN FINANZAS PERSONALES --- */} - {/* FINANZAS CLAN — SOLO SI HAY CLAN SELECCIONADO */} + {/* --- INICIO: SECCIÓN FINANZAS DEL CLAN --- */} {!activeClanId ? (
-

- Selecciona un clan en Grupos para ver sus finanzas 💰 -

+

Selecciona un clan en la página de "Grupos" para ver sus finanzas.

) : (

Finanzas: {store.clans.find(c => c.id === activeClanId)?.name}

-
- + {/* Bote Común */}
-
+

Bote Común

-

- {currentBote.toFixed(2)} € -

-
- + {/* Añadir Gasto de Clan */}
-
-

Añadir Gasto del Clan

- -
e.preventDefault()} className="mt-3"> +
+

Añadir Gasto de Clan

+
- - setClanConcept(e.target.value)} - placeholder="Ej: Pizzas" /> + + setClanConcept(e.target.value)} placeholder="Ej: Pizzas para la reunión" />
-
- - setClanAmount(e.target.value)} - placeholder="0" /> + + setClanAmount(e.target.value)} + placeholder="0" + /> +
+
+
- -
- + {/* Historial de Gastos de Clan */}
-

Historial del Clan

-

- Función pendiente de restaurar 🔧 -

+

Historial de Gastos del Clan

+
    + {activeExpenses.length > 0 ? activeExpenses.map(expense => ( +
  • +
    + {expense.concept} + Pagado por: {expense.paidBy} +
    + -{expense.amount.toFixed(2)} € +
  • + )) : ( +

    No hay gastos registrados para este clan.

    + )} +
- + {/* Balances de Clan */}
-

Balance entre miembros

-

- Función pendiente de restaurar 🔧 -

+

Balances del Grupo

+
    + {activeBalances.length > 0 ? activeBalances.map(balance => ( +
  • + {balance.name} + = 0 ? 'balance-positive' : 'balance-negative'}> + {balance.amount.toFixed(2)} € + +
  • + )) : ( +

    No hay balances calculados.

    + )} +
-
)}
); -}; +}; \ No newline at end of file diff --git a/src/front/pages/Layout.jsx b/src/front/pages/Layout.jsx index 03fb371b91..98e15408e5 100644 --- a/src/front/pages/Layout.jsx +++ b/src/front/pages/Layout.jsx @@ -1,14 +1,12 @@ import { Outlet } from "react-router-dom/dist" import { AnimatePresence, motion } from "framer-motion" import ScrollToTop from "../components/ScrollToTop" -import { Navbar } from "../components/Navbar" import { Footer } from "../components/Footer" // Base component that maintains the navbar and footer throughout the page and the scroll to top functionality. export const Layout = () => { - return ( - - + return ( + {
- ) + ) } \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index 182419bec4..3dd6c83560 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,14 +1,10 @@ export const initialStore = () => { return { - message: null, - todos: [ - { id: 1, title: "Make the bed", background: null }, - { id: 2, title: "Do my homework", background: null } - ], profile: { name: "Nombre de Usuario", email: "correo@delusuario.com", - presentation: "Breve presentación del usuario. Haz clic en 'Editar Perfil' para cambiar esto.", + presentation: + "Breve presentación del usuario. Haz clic en 'Editar Perfil' para cambiar esto.", location: "Vive en Madrid", age: 26, phone: "666 000 666", @@ -16,136 +12,274 @@ export const initialStore = () => { social: { instagram: "usuario_ig", twitter: "usuario_tw", - facebook: "usuario_fb" + facebook: "usuario_fb", }, - avatar: "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png" + avatar: + "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png", }, clans: [ - { id: 1, name: "Los geek's", category: "Grupo de Trabajo", members: 4, created: "2025-11-03" }, - { id: 2, name: "Familia", category: "Familia", members: 3, created: "2025-10-01" }, - { id: 3, name: "Novia/o", category: "Social", members: 2, created: "2025-09-15" } + { + id: 1, + name: "Los geek's", + category: "Grupo de Trabajo", + members: 4, + created: "2025-11-03", + }, + { + id: 2, + name: "Familia", + category: "Familia", + members: 3, + created: "2025-10-01", + }, + { + id: 3, + name: "Novia/o", + category: "Social", + members: 2, + created: "2025-09-15", + }, ], activeClanId: 1, - tasks: [ - { id: 101, clanId: 1, title: "Terminar el proyecto final", completed: false }, - { id: 102, clanId: 1, title: "Preparar la presentación", completed: false }, - { id: 103, clanId: 1, title: "Revisar el backend", completed: true }, + userTasks: [ + { id: 1, title: "Make the bed", completed: false }, + { id: 2, title: "Do my homework", completed: false }, + // Añadimos dos tareas más para que el contador "sin hacer" sea 4 + { id: 3, title: "Comprar comida", completed: false }, + { id: 4, title: "Llamar al banco", completed: false }, + ], + clanTasks: [ + { + id: 101, + clanId: 1, + title: "Terminar el proyecto final", + completed: false, + }, + { + id: 102, + clanId: 1, + title: "Preparar la presentación", + completed: false, + }, + { id: 103, clanId: 1, title: "Revisar el backend", completed: true }, // <-- Tarea completada { id: 104, clanId: 2, title: "Comprar pan", completed: false }, - { id: 105, clanId: 2, title: "Llamar a la abuela", completed: false }, - { id: 106, clanId: 3, title: "Reservar restaurante para el viernes", completed: false } ], - personalBote: 100.00, + personalBote: 100.0, + commonBote: { + 1: 150.0, + 2: 75.5, + 3: 200.0, + }, // 🔥 Recuperado del revert token: localStorage.getItem("token") || null, - tareas: [ + + // Finanzas + personalBote: 119.58, + personalExpenses: [ + // <-- NUEVO: Para Finanzas Personales { - id: 1, - titulo: "Hacer la compra", - descripcion: "", - fecha: "2025-11-13", - hora: "10:00", - direccion: "Calle Santoña 56, Madrid", - invitados: ["juan@gmail.com"], - lat: 40.3926, - lng: -3.7016, + id: 401, + concept: "Café de la mañana", + amount: 2.5, + date: "2025-11-15", }, + { id: 402, concept: "Ticket de metro", amount: 1.5, date: "2025-11-14" }, + ], + expenses: [ + // Gastos de Clan { - id: 2, - titulo: "Quedada para el cine", - descripcion: "", - fecha: "2025-11-14", - hora: "18:00", - direccion: "Calle del Santuario 70, Madrid", - invitados: ["ana@gmail.com", "luis@gmail.com"], - lat: 40.3926, - lng: -3.7016, - } - ] + id: 201, + clanId: 1, + concept: "Pizzas para reunión", + amount: 35.5, + paidBy: "Nombre de Usuario", + date: "2025-11-10", + }, + { + id: 202, + clanId: 1, + concept: "Suscripción a servicio API", + amount: 15.0, + paidBy: "Amigo Uno", + date: "2025-11-05", + }, + { + id: 203, + clanId: 2, + concept: "Compra supermercado", + amount: 50.2, + paidBy: "Familia", + date: "2025-11-01", + }, + ], + balances: [ + { id: 301, clanId: 1, name: "Nombre de Usuario", amount: 15.0 }, + { id: 302, clanId: 1, name: "Amigo Uno", amount: -10.0 }, + ], }; }; // Reducer que maneja todas las acciones export default function storeReducer(store, action = {}) { switch (action.type) { - case 'set_hello': + case "set_hello": return { ...store, message: action.payload }; - case 'add_task': - const { id, color } = action.payload + case "ADD_USER_TASK": + const newUserTask = { + id: new Date().getTime(), + title: action.payload.title, + completed: false, + }; return { ...store, - todos: store.todos.map((todo) => (todo.id === id ? { ...todo, background: color } : todo)) + userTasks: [...store.userTasks, newUserTask], + }; + + case "TOGGLE_USER_TASK": + return { + ...store, + userTasks: store.userTasks.map((task) => { + return task.id === action.payload.taskId + ? { ...task, completed: !task.completed } + : task; + }), + }; + case "DELETE_USER_TASK": + return { + ...store, + userTasks: store.userTasks.filter( + (task) => task.id !== action.payload.taskId + ), }; // Acciones de Perfil - case 'UPDATE_PROFILE': + case "UPDATE_PROFILE": return { ...store, - profile: { ...store.profile, ...action.payload } + profile: { ...store.profile, ...action.payload }, }; - case 'UPDATE_PERSONAL_BOTE': + case "UPDATE_PERSONAL_BOTE": return { ...store, - personalBote: parseFloat(action.payload.newBote) + personalBote: parseFloat(action.payload.newBote), }; // Acciones de Clanes / Grupos - case 'CREATE_CLAN': + case "CREATE_CLAN": const newClan = { id: new Date().getTime(), ...action.payload, - members: 1 + members: 1, }; return { ...store, - clans: [...store.clans, newClan] + clans: [...store.clans, newClan], }; - case 'JOIN_CLAN': + case "JOIN_CLAN": console.log("Intentando unirse al clan con código:", action.payload.code); return store; - case 'SET_ACTIVE_CLAN': + case "SET_ACTIVE_CLAN": return { ...store, - activeClanId: action.payload.clanId + activeClanId: action.payload.clanId, }; - case 'DELETE_CLAN': + case "DELETE_CLAN": if (!store.activeClanId) return store; - const remainingClans = store.clans.filter(clan => clan.id !== store.activeClanId); - const remainingTasks = store.tasks.filter(task => task.clanId !== store.activeClanId); + const remainingClans = store.clans.filter( + (clan) => clan.id !== store.activeClanId + ); + const remainingTasks = store.tasks.filter( + (task) => task.clanId !== store.activeClanId + ); return { ...store, clans: remainingClans, tasks: remainingTasks, - activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null + activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null, }; - // Acciones de Tareas de Clan - case 'ADD_TASK_TO_CLAN': + // Acciones de Tareas de Clan + case "ADD_TASK_TO_CLAN": if (!store.activeClanId) return store; const newTask = { id: new Date().getTime(), clanId: store.activeClanId, title: action.payload.title, - completed: false + completed: false, }; + return { ...store, clanTasks: [...store.clanTasks, newTask] }; + + case "DELETE_CLAN_TASK": + const updatedTasks = store.clanTasks.filter( + (task) => task.id !== action.payload.taskId + ); + return { ...store, clanTasks: updatedTasks }; + case "TOGGLE_CLAN_TASK": return { ...store, - tasks: [...store.tasks, newTask] + clanTasks: store.clanTasks.map((task) => { + console.log(action.payload.taskId); + return task.id === action.payload.taskId + ? { ...task, completed: !task.completed } + : task; + }), }; - case 'DELETE_CLAN_TASK': - const updatedTasks = store.tasks.filter(task => task.id !== action.payload.taskId); + case "UPDATE_PERSONAL_BOTE": + return { ...store, personalBote: parseFloat(action.payload.newBote) }; + case "ADD_PERSONAL_EXPENSE": + const newPersonalExpense = { + id: new Date().getTime(), + concept: action.payload.concept, + amount: parseFloat(action.payload.amount), + date: new Date().toISOString().split("T")[0], + }; + return { + ...store, + personalBote: store.personalBote - newPersonalExpense.amount, + personalExpenses: [newPersonalExpense, ...store.personalExpenses], + }; + case "ADD_EXPENSE": // Gasto de Clan + if (!store.activeClanId) return store; + const { concept, amount } = action.payload; + const newExpense = { + id: new Date().getTime(), + clanId: store.activeClanId, + concept: concept, + amount: parseFloat(amount), + paidBy: store.profile.name, + date: new Date().toISOString().split("T")[0], // <-- Añadido + }; + const newBoteAmount = + (store.commonBote[store.activeClanId] || 0) - newExpense.amount; + return { + ...store, + expenses: [newExpense, ...store.expenses], + commonBote: { + ...store.commonBote, + [store.activeClanId]: newBoteAmount, + }, + }; + case "ADD_TO_BOTE": // Añadir a Bote de Clan + if (!store.activeClanId) return store; + const newTotal = + (store.commonBote[store.activeClanId] || 0) + + parseFloat(action.payload.amount); return { ...store, - tasks: updatedTasks + commonBote: { + ...store.commonBote, + [store.activeClanId]: newTotal, + }, }; default: - throw Error('Unknown action.'); + throw Error("Unknown action."); } } From 72592709b16781a7060b2e7d95d073e845a8d7a9 Mon Sep 17 00:00:00 2001 From: Horacio Rodriguez <96556399+AngelRRand@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:45:17 +0000 Subject: [PATCH 35/54] dashbord --- src/front/components/ModalCreateTask.jsx | 94 +++++++ src/front/index.css | 13 + src/front/pages/Dashboard.jsx | 14 +- src/front/pages/Layout.jsx | 1 + src/front/pages/Profile.jsx | 301 +++++++++++++++++++---- src/front/pages/TaskUser.jsx | 4 +- src/front/store.js | 20 ++ src/front/styles/Form.css | 31 +-- src/front/styles/Landing.css | 7 +- src/front/styles/ProfileGroups.css | 1 - 10 files changed, 411 insertions(+), 75 deletions(-) create mode 100644 src/front/components/ModalCreateTask.jsx diff --git a/src/front/components/ModalCreateTask.jsx b/src/front/components/ModalCreateTask.jsx new file mode 100644 index 0000000000..1f8216dad6 --- /dev/null +++ b/src/front/components/ModalCreateTask.jsx @@ -0,0 +1,94 @@ +import React, { useState } from "react"; +import useGlobalReducer from "../hooks/useGlobalReducer"; +import GoogleMaps from "../components/GoogleMaps"; + +function ModalCreateTask({ setShowTaskModal }) { + const { dispatch } = useGlobalReducer(); + const [titulo, setTitulo] = useState(""); + const [descripcion, setDescripcion] = useState(""); + const [direccion, setDireccion] = useState(""); + const [lat, setLat] = useState(""); + const [lng, setLng] = useState(""); + const [msg, setMsg] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + + dispatch({ type: 'ADD_USER_TASK', payload: { title: newTaskTitle } }); + + setTitulo(""); + setDescripcion(""); + setDireccion(""); + setLat(""); + setLng(""); + setMsg("Tarea creada (mock)"); + + }; + + const handleInvitadosChange = (e) => { + let value = e.target.value; + + const emails = value.split(',').map(i => i.trim()).filter(i => i); + if (emails.length > 1 && !value.trim().endsWith(',')) { + value = value + ', '; + } + setInvitados(value); + }; + + return ( +
+
+
+
+ Añadir Tarea de Clan +
+ +
+ setTitulo(e.target.value)} style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} /> +
+
+
+
+
+
+
+
+
Redes
+
+
+
+
+
+
+
+
+ + +
+ +
+
+ )} - {/* FINANZAS PERSONALES */} -
-

Bote Personal

-
-

{store.personalBote.toFixed(2)} €

- - Gastos: -{totalPersonalExpenses.toFixed(2)} € - + {/* --- MODAL PARA EDITAR BOTE --- */} + {showBoteModal && ( +
+
+
+
+
+
Editar Saldo del Bote Personal
+ +
+
+
+ + setBoteAmount(e.target.value)} + placeholder="0" + /> +
+
+
+ + +
+
+
+ )} + + {(showModal || showBoteModal) &&
} + + {/* --- CONTENIDO DE LA PÁGINA (NUEVO LAYOUT) --- */} +
+
+ {/* --- COLUMNA IZQUIERDA (Perfil + Amigos) --- */} +
+ {/* Info Perfil */} +
+ Foto de perfil +

{store.profile.name}

+

{store.profile.email}

+ +
+ {/* Barra de Búsqueda Amigos (Placeholder) */} + + {/* Lista de Amigos */} +
+
Amigos activos
+ {store.friends.map(friend => ( + + ))} +
+
- {/* TAREAS PERSONALES */} -
-

Mis Tareas

-
    - {personalTasks.length > 0 ? ( - personalTasks.map(task => ( -
  • - {task.title} - - {task.completed ? "Completada" : "Pendiente"} - -
  • - )) - ) : ( -

    - Aún no tienes tareas asignadas. -

    - )} -
+ {/* --- COLUMNA DERECHA (Cuadrícula 2x2 + Bote) --- */} +
+ {/* --- BARRA DE RESUMEN (ACTUALIZADA) --- */} +
+
{tasksCompleted}
Tareas completadas
+
{tasksPending}
Tareas sin hacer
+ {/* "Logros" eliminado */} +
{clanCount}
Clan
+
+ + {/* --- INICIO CUADRÍCULA 2x2 (Responsive) --- */} +
+ {/* Detalles */} +
+
+

Detalles

+

{store.profile.presentation}

+
+

{store.profile.location}

+

{store.profile.age} años

+

{store.profile.phone}

+

{store.profile.gender}

+
+
+ {/* Mensajes */} +
+
+

Mensajes

+

No hay mensajes nuevos.

+
+
+ {/* Otras Redes */} +
+
+

Otras redes

+

{store.profile.social.instagram}

+

{store.profile.social.twitter}

+

{store.profile.social.facebook}

+
+
+ {/*
+
+

Calendario

+ +
+
*/} +
+ {/* --- FIN CUADRÍCULA 2x2 --- */} + + {/* --- BOTE PERSONAL --- */} +
+
+
+

Saldo del Bote Personal

+ {/* --- BOTÓN EDITAR BOTE (ARREGLADO) --- */} + +
+

+ {store.personalBote.toFixed(2)} € +

+
+
+
); -}; +}; \ No newline at end of file diff --git a/src/front/pages/TaskUser.jsx b/src/front/pages/TaskUser.jsx index 92167179b6..12c1390c5c 100644 --- a/src/front/pages/TaskUser.jsx +++ b/src/front/pages/TaskUser.jsx @@ -35,8 +35,8 @@ export const TaskUser = () => {

Tareas

- {store.tareas && store.tareas.length > 0 ? ( - store.tareas.map(tarea => ( + {store.userTasks && store.userTasks.length > 0 ? ( + store.userTasks.map(tarea => (
diff --git a/src/front/store.js b/src/front/store.js index 3dd6c83560..23ea33c1f2 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -17,6 +17,26 @@ export const initialStore = () => { avatar: "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png", }, + friends: [ + { + id: 1, + name: "Amigo Uno", + status: "online", + avatar: "https://i.pravatar.cc/150?img=11", + }, + { + id: 2, + name: "Amiga Dos", + status: "offline", + avatar: "https://i.pravatar.cc/150?img=12", + }, + { + id: 3, + name: "Amigo Tres", + status: "online", + avatar: "https://i.pravatar.cc/150?img=13", + }, + ], clans: [ { id: 1, diff --git a/src/front/styles/Form.css b/src/front/styles/Form.css index 3a8a6fc76f..5cc34d3bb7 100644 --- a/src/front/styles/Form.css +++ b/src/front/styles/Form.css @@ -1,32 +1,18 @@ -body { - min-height: 100vh; - margin: 0; - background: - linear-gradient(180deg, #1e91ed 10%, #7f00b2 90%); - background-repeat: no-repeat; - background-attachment: fixed; - color: white; - font-family: 'Arial', sans-serif; - transition: background 0.5s ease, color 0.5s ease; -} - .Form { width: 100%; max-width: 600px; min-height: 60vh; - background: rgba(255, 255, 255, 0.2); - backdrop-filter: blur(10px); + background: white; border-radius: 16px; padding: 60px 40px; - box-shadow: 0 10px 32px rgba(0, 0, 0, 0.50); - margin: 0 auto; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; justify-content: center; } .button { - background-color: #6366F1; + background-color: #6366f1; color: white; border: none; border-radius: 8px; @@ -41,7 +27,10 @@ body { background-color: #4f46e5; } -h1, h2, h3, p { +h1, +h2, +h3, +p { transition: color 0.5s ease; } @@ -50,7 +39,7 @@ h1, h2, h3, p { color: #000; padding: 20px; border-radius: 12px; - box-shadow: 0 4px 20px rgba(0,0,0,0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); min-height: 150px; display: flex; flex-direction: column; @@ -60,9 +49,9 @@ h1, h2, h3, p { .card:hover { transform: translateY(-5px); - box-shadow: 0 8px 25px rgba(0,0,0,0.2); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); } .section { transition: background 0.5s ease, color 0.5s ease; -} \ No newline at end of file +} diff --git a/src/front/styles/Landing.css b/src/front/styles/Landing.css index c290afc044..e1280c1969 100644 --- a/src/front/styles/Landing.css +++ b/src/front/styles/Landing.css @@ -2,7 +2,6 @@ padding: 40px 20px; margin: 0 auto; min-height: 100vh; - background: linear-gradient(180deg, #1e91ed, #7f00b2); color: white; } @@ -31,7 +30,7 @@ background: rgba(255, 255, 255, 0.9); color: #000; border-radius: 12px; - box-shadow: 0 4px 20px rgba(0,0,0,1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 1); overflow: hidden; display: flex; flex-direction: column; @@ -86,9 +85,9 @@ .landing-cta button { padding: 14px 32px; font-size: 18px; - background: #6366F1; + background: #6366f1; color: white; border: none; border-radius: 8px; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/front/styles/ProfileGroups.css b/src/front/styles/ProfileGroups.css index de88005e1b..27548e4401 100644 --- a/src/front/styles/ProfileGroups.css +++ b/src/front/styles/ProfileGroups.css @@ -3,7 +3,6 @@ body { margin: 0; - background: linear-gradient(180deg, #1e91ed 10%, #7f00b2 90%); background-repeat: no-repeat; background-attachment: fixed; color: white; From 4e12a35c03631bc6d53a6009a8914f09c57a4b27 Mon Sep 17 00:00:00 2001 From: Horacio Rodriguez <96556399+AngelRRand@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:26:39 +0000 Subject: [PATCH 36/54] hola --- migrations/versions/0763d677d453_.py | 35 ----- src/api/admin.py | 6 +- src/api/models.py | 176 +++++++++++++----------- src/api/routes.py | 38 +----- src/api/routesUser.py | 86 +++++++----- src/front/components/Form.jsx | 197 ++++++++++++++++++++------- src/front/pages/Dashboard.jsx | 13 +- src/front/pages/Login.jsx | 6 +- src/front/pages/Register.jsx | 6 +- src/front/store.js | 4 +- src/front/styles/Form.css | 16 +++ 11 files changed, 332 insertions(+), 251 deletions(-) delete mode 100644 migrations/versions/0763d677d453_.py 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/src/api/admin.py b/src/api/admin.py index a90b2888b5..16ce774d76 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,7 +1,7 @@ import os from flask_admin import Admin -from .models import db, User, Tareas, TareasAsignadas, Mision, Evento, Prioridad, Estado, Perfil, Grupo, Categoria, Clan +from .models import db, User, Tareas, TareasAsignadas, Mision, Evento, Prioridad, Estado, Grupo, Categoria, Clan from flask_admin.contrib.sqla import ModelView @@ -15,7 +15,6 @@ def setup_admin(app): # You can duplicate that line to add mew models # admin.add_view(ModelView(YourModelName, db.session)) - admin.add_view(ModelView(Perfil, db.session)) admin.add_view(ModelView(Tareas, db.session)) admin.add_view(ModelView(TareasAsignadas, db.session)) admin.add_view(ModelView(Mision, db.session)) @@ -25,6 +24,3 @@ def setup_admin(app): admin.add_view(ModelView(Grupo, db.session)) admin.add_view(ModelView(Clan, db.session)) admin.add_view(ModelView(Categoria, db.session)) - - - diff --git a/src/api/models.py b/src/api/models.py index 14e5175d86..f14043cb6a 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -5,81 +5,75 @@ db = SQLAlchemy() + class User(db.Model): __tablename__ = 'user' id: Mapped[int] = mapped_column(Integer, primary_key=True) - email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + email: Mapped[str] = mapped_column( + String(100), unique=True, nullable=False) password: Mapped[str] = mapped_column(String(255), nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) - uuid_contraseña: Mapped[str] = mapped_column(String(255), nullable=True) - db_perfil_user: Mapped[list['Perfil']] = relationship(back_populates='db_user_perfil') - db_clan_user: Mapped[list['Clan']] = relationship(back_populates='db_user_clan') - db_tareas_asignadas_user: Mapped[list['TareasAsignadas']] = relationship(back_populates='db_user_tareas_asignadas') - - def __repr__(self): - return f'{self.email}' - - def serialize(self): - return { - "id": self.id, - "email": self.email - } - -class Perfil(db.Model): - __tablename__ = 'perfil' - id: Mapped[int] = mapped_column(Integer, primary_key=True) - nombre: Mapped[str] = mapped_column(String(80), nullable=False) - foto: Mapped[str] = mapped_column(String(255), nullable=True) - presentacion: Mapped[str] = mapped_column(String(250), nullable=True) - telefono: Mapped[str] = mapped_column(String(20), nullable=True) - edad: Mapped[int] = mapped_column(Integer, nullable=True) - ciudad: Mapped[str] = mapped_column(String(80), nullable=True) - genero: Mapped[str] = mapped_column(String(80), nullable=True) + name: Mapped[str] = mapped_column(String(80), nullable=False) + photo: Mapped[str] = mapped_column(String(255), nullable=True) + bio: Mapped[str] = mapped_column(String(250), nullable=True) + phone: Mapped[str] = mapped_column(String(20), nullable=True) + age: Mapped[int] = mapped_column(Integer, nullable=True) + city: Mapped[str] = mapped_column(String(80), nullable=True) + gender: Mapped[str] = mapped_column(String(80), nullable=True) twitter: Mapped[str] = mapped_column(String(80), nullable=True) facebook: Mapped[str] = mapped_column(String(80), nullable=True) instagram: Mapped[str] = mapped_column(String(80), nullable=True) - user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) - db_user_perfil: Mapped['User'] = relationship(back_populates='db_perfil_user') + db_clan_user: Mapped[list['Clan']] = relationship( + back_populates='db_user_clan') + db_tareas_asignadas_user: Mapped[list['TareasAsignadas']] = relationship( + back_populates='db_user_tareas_asignadas') def __repr__(self): - return f'{self.nombre}' - + return f'{self.email}' + def serialize(self): return { "id": self.id, - "nombre": self.nombre, - "foto": self.foto, - "presentacion": self.presentacion, - "telefono": self.telefono, - "edad": self.edad, - "ciudad": self.ciudad, - "genero": self.genero, + "email": self.email, + "name": self.name, + "photo": self.photo, + "bio": self.bio, + "phone": self.phone, + "age": self.age, + "city": self.city, + "gender": self.gender, "twitter": self.twitter, "facebook": self.facebook, "instagram": self.instagram, - "user_id": self.user_id } + class Tareas(db.Model): __tablename__ = 'tareas' id: Mapped[int] = mapped_column(Integer, primary_key=True) titulo: Mapped[str] = mapped_column(String(80), nullable=False) - evento_id: Mapped[int] = mapped_column(ForeignKey('evento.id'), nullable=True) - db_evento_tareas: Mapped['Evento'] = relationship(back_populates='db_tareas_evento') + evento_id: Mapped[int] = mapped_column( + ForeignKey('evento.id'), nullable=True) + db_evento_tareas: Mapped['Evento'] = relationship( + back_populates='db_tareas_evento') fecha: Mapped[datetime] = mapped_column(DateTime, nullable=True) descripcion: Mapped[str] = mapped_column(String(250), nullable=True) - prioridad_id: Mapped[int] = mapped_column(ForeignKey('prioridad.id'), nullable=True) - db_prioridad_tareas: Mapped['Prioridad'] = relationship(back_populates='db_tareas_prioridad') - estado_id: Mapped[int] = mapped_column(ForeignKey('estado.id'), nullable=True) - db_estado_tareas: Mapped['Estado'] = relationship(back_populates='db_tareas_estado') + prioridad_id: Mapped[int] = mapped_column( + ForeignKey('prioridad.id'), nullable=True) + db_prioridad_tareas: Mapped['Prioridad'] = relationship( + back_populates='db_tareas_prioridad') + estado_id: Mapped[int] = mapped_column( + ForeignKey('estado.id'), nullable=True) + db_estado_tareas: Mapped['Estado'] = relationship( + back_populates='db_tareas_estado') imagen: Mapped[str] = mapped_column(String(255), nullable=True) - db_mision_tareas: Mapped[list['Mision']] = relationship(back_populates='db_tareas_mision') - db_tareas_asignadas_tareas: Mapped[list['TareasAsignadas']] = relationship(back_populates='db_tareas_tareas_asignadas') - + db_mision_tareas: Mapped[list['Mision']] = relationship( + back_populates='db_tareas_mision') + db_tareas_asignadas_tareas: Mapped[list['TareasAsignadas']] = relationship( + back_populates='db_tareas_tareas_asignadas') def __repr__(self): return f'{self.titulo}' - + def serialize(self): return { "id": self.id, @@ -91,68 +85,79 @@ def serialize(self): "prioridad_id": self.prioridad_id, "estado_id": self.estado_id, } - + + class Estado(db.Model): __tablename__ = 'estado' id: Mapped[int] = mapped_column(Integer, primary_key=True) tipo: Mapped[str] = mapped_column(String(20), nullable=True) - db_tareas_estado: Mapped[list['Tareas']] = relationship(back_populates='db_estado_tareas') + db_tareas_estado: Mapped[list['Tareas']] = relationship( + back_populates='db_estado_tareas') def __repr__(self): return f'{self.tipo}' - + def serialize(self): return { "id": self.id, "tipo": self.tipo } - + + class Evento(db.Model): __tablename__ = 'evento' id: Mapped[int] = mapped_column(Integer, primary_key=True) titulo: Mapped[str] = mapped_column(String(20), nullable=False) lugar: Mapped[str] = mapped_column(String(100), nullable=False) - db_tareas_evento: Mapped[list['Tareas']] = relationship(back_populates='db_evento_tareas') + db_tareas_evento: Mapped[list['Tareas']] = relationship( + back_populates='db_evento_tareas') def __repr__(self): return f'{self.titulo}' - + def serialize(self): return { "id": self.id, "titulo": self.titulo, "lugar": self.lugar } - + + class Prioridad(db.Model): __tablename__ = 'prioridad' id: Mapped[int] = mapped_column(Integer, primary_key=True) nivel: Mapped[str] = mapped_column(String(20), nullable=False) - db_tareas_prioridad: Mapped[list['Tareas']] = relationship(back_populates='db_prioridad_tareas') + db_tareas_prioridad: Mapped[list['Tareas']] = relationship( + back_populates='db_prioridad_tareas') def __repr__(self): return f'{self.nivel}' - + def serialize(self): return { "id": self.id, "nivel": self.nivel, - } + } + class Grupo(db.Model): __tablename__ = 'grupo' id: Mapped[int] = mapped_column(Integer, primary_key=True) - nombre: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + nombre: Mapped[str] = mapped_column( + String(100), unique=True, nullable=False) categoria_id: Mapped[int] = mapped_column(ForeignKey('categoria.id')) - db_categoria_grupo: Mapped['Categoria'] = relationship(back_populates='db_grupo_categoria') + db_categoria_grupo: Mapped['Categoria'] = relationship( + back_populates='db_grupo_categoria') fecha: Mapped[datetime] = mapped_column(DateTime, nullable=True) codigo: Mapped[int] = mapped_column(Integer, nullable=True) - db_clan_grupo: Mapped[list['Clan']] = relationship(back_populates='db_grupo_clan') - db_mision_grupo: Mapped[list['Mision']] = relationship(back_populates='db_grupo_mision') + db_clan_grupo: Mapped[list['Clan']] = relationship( + back_populates='db_grupo_clan') + db_mision_grupo: Mapped[list['Mision']] = relationship( + back_populates='db_grupo_mision') def __repr__(self): return f'{self.nombre}' - + def serialize(self): return { "id": self.id, @@ -161,75 +166,82 @@ def serialize(self): "fecha": self.fecha, "codigo": self.codigo } - + + class Categoria(db.Model): __tablename__ = 'categoria' id: Mapped[int] = mapped_column(Integer, primary_key=True) nombre: Mapped[str] = mapped_column(String(80), nullable=False) - db_grupo_categoria: Mapped[list['Grupo']] = relationship(back_populates='db_categoria_grupo') + db_grupo_categoria: Mapped[list['Grupo']] = relationship( + back_populates='db_categoria_grupo') def __repr__(self): return f'{self.nombre}' - + def serialize(self): return { "id": self.id, "nombre": self.nombre, } - + + class Clan(db.Model): __tablename__ = 'clan' id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) db_user_clan: Mapped['User'] = relationship(back_populates='db_clan_user') grupo_id: Mapped[int] = mapped_column(ForeignKey('grupo.id')) - db_grupo_clan: Mapped['Grupo'] = relationship(back_populates='db_clan_grupo') - + db_grupo_clan: Mapped['Grupo'] = relationship( + back_populates='db_clan_grupo') def __repr__(self): - return f'User = {self.user_id} y Grupo = {self.grupo_id}' - + return f'User = {self.user_id} y Grupo = {self.grupo_id}' + def serialize(self): return { "id": self.id, "user_id": self.user_id, "grupo_id": self.grupo_id } - + + class Mision(db.Model): __tablename__ = 'mision' id: Mapped[int] = mapped_column(Integer, primary_key=True) tareas_id: Mapped[int] = mapped_column(ForeignKey('tareas.id')) - db_tareas_mision: Mapped['Tareas'] = relationship(back_populates='db_mision_tareas') + db_tareas_mision: Mapped['Tareas'] = relationship( + back_populates='db_mision_tareas') grupo_id: Mapped[int] = mapped_column(ForeignKey('grupo.id')) - db_grupo_mision: Mapped['Grupo'] = relationship(back_populates='db_mision_grupo') - + db_grupo_mision: Mapped['Grupo'] = relationship( + back_populates='db_mision_grupo') def __repr__(self): return f'Grupo = {self.grupo_id} y Tarea = {self.tareas_id}' - + def serialize(self): return { "id": self.id, "tareas_id": self.tareas_id, "grupo_id": self.grupo_id } - + + class TareasAsignadas(db.Model): __tablename__ = 'tareas_asignadas' id: Mapped[int] = mapped_column(Integer, primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey('user.id')) - db_user_tareas_asignadas: Mapped['User'] = relationship(back_populates='db_tareas_asignadas_user') + db_user_tareas_asignadas: Mapped['User'] = relationship( + back_populates='db_tareas_asignadas_user') tareas_id: Mapped[int] = mapped_column(ForeignKey('tareas.id')) - db_tareas_tareas_asignadas: Mapped['Tareas'] = relationship(back_populates='db_tareas_asignadas_tareas') - + db_tareas_tareas_asignadas: Mapped['Tareas'] = relationship( + back_populates='db_tareas_asignadas_tareas') def __repr__(self): return f'User = {self.user_id} y Tarea = {self.tareas_id}' - + def serialize(self): return { "id": self.id, "user_id": self.user_id, "tareas_id": self.tareas_id - } \ No newline at end of file + } diff --git a/src/api/routes.py b/src/api/routes.py index 5d8ee7378b..f6205c5118 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -4,8 +4,6 @@ import jwt from werkzeug.security import check_password_hash from api.models import db, User -from werkzeug.security import generate_password_hash - def token_requerido(f): @@ -28,6 +26,7 @@ def wrapper(*args, **kwargs): api = Blueprint('api', __name__) reset_tokens = {} + @api.route('/user', methods=['GET']) @token_requerido def get_user(): @@ -61,7 +60,7 @@ def update_user(): return jsonify({"msg": "El email ya está en uso"}), 400 user.email = new_email if new_password: - from werkzeug.security import generate_password_hash + user.password = generate_password_hash(new_password) db.session.commit() @@ -101,39 +100,6 @@ def reset_password(): return jsonify({"msg": "Cambio de contraseña"}), 200 -@api.route('/login', methods=['POST']) -def login(): - data = request.get_json() - email = data.get('email') - password = data.get('password') - if not email or not password: - return jsonify({"msg": "Falta correo o contraseña"}), 400 - user = User.query.filter_by(email=email).first() - if not user or not check_password_hash(user.password, password): - return jsonify({"msg": "Usuario o contraseña incorrectos"}), 401 - token = jwt.encode({ - 'user_id': user.id, - 'exp': datetime.now(timezone.utc) + timedelta(minutes=15) - }, SECRET_KEY, algorithm="HS256") - return jsonify({"token": token}) - - -@api.route('/register', methods=['POST']) -def register(): - data = request.get_json() - email = data.get('email') - password = data.get('password') - if not email or not password: - return jsonify({"msg": "Falta correo o contraseña"}), 400 - if User.query.filter_by(email=email).first(): - return jsonify({"msg": "El usuario ya existe"}), 400 - hashed_password = generate_password_hash(password) - new_user = User(email=email, password=hashed_password, is_active=True) - db.session.add(new_user) - db.session.commit() - return jsonify({"msg": "Usuario registrado exitosamente"}), 201 - - @api.route('/hello', methods=['POST', 'GET']) @token_requerido def handle_hello(): diff --git a/src/api/routesUser.py b/src/api/routesUser.py index 9f92fb60bd..fb2c9bfc5e 100644 --- a/src/api/routesUser.py +++ b/src/api/routesUser.py @@ -1,43 +1,43 @@ from flask import request, jsonify, Blueprint -import secrets from datetime import datetime, timedelta, timezone -import jwt -from werkzeug.security import check_password_hash -from api.models import db, Perfil, Grupo, User, Clan -from werkzeug.security import generate_password_hash +from api.models import db, User +from werkzeug.security import generate_password_hash, check_password_hash from flask_cors import CORS +import jwt api_user = Blueprint('apiUser', __name__) # Allow CORS requests to this API CORS(api_user) +SECRET_KEY = "super-secret-key" -@api_user.route('//perfil', methods=['POST']) -def crear_perfil(user_id): - varUser = User.query.get(user_id) - if varUser is None: - return jsonify({"msg": f"Usuario con ID {user_id} no existe"}), 404 - perfil_existente = Perfil.query.filter_by(user_id=user_id).first() - if perfil_existente is not None: - return jsonify({"msg": "Este usuario ya tiene un perfil creado"}), 400 + +@api_user.route('/register', methods=['POST']) +def crear_perfil(): body = request.get_json() - if body is None: - return jsonify({"msg": "No enviaste un body"}), 400 - if "nombre" not in body or not body["nombre"].strip(): - return jsonify({"msg": "El campo 'nombre' es obligatorio"}), 400 - - nuevo_perfil = Perfil( - nombre = body.get("nombre"), - foto = body.get("foto"), - presentacion = body.get("presentacion"), - telefono = body.get("telefono"), - edad = body.get("edad"), - ciudad = body.get("ciudad"), - genero = body.get("genero"), - twitter = body.get("twitter"), - facebook = body.get("facebook"), - instagram = body.get("instagram"), - user_id = user_id + email = body.get('email') + password = body.get('password') + if not email or not password: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + if User.query.filter_by(email=email).first(): + return jsonify({"msg": "El usuario ya existe"}), 400 + hashed_password = generate_password_hash(password) + + print(body) + + nuevo_perfil = User( + email=email, + password=hashed_password, + name=body.get("name"), + photo=body.get("photo"), + bio=body.get("bio"), + phone=body.get("phone"), + age=body.get("age"), + city=body.get("city"), + gender=body.get("gender"), + twitter=body.get("twitter"), + facebook=body.get("facebook"), + instagram=body.get("instagram"), ) db.session.add(nuevo_perfil) @@ -47,23 +47,42 @@ def crear_perfil(user_id): "msg": "Perfil creado correctamente", "perfil": nuevo_perfil.serialize() }), 201 + +@api_user.route('/login', methods=['POST']) +def login(): + data = request.get_json() + email = data.get('email') + password = data.get('password') + if not email or not password: + return jsonify({"msg": "Falta correo o contraseña"}), 400 + user = User.query.filter_by(email=email).first() + if not user or not check_password_hash(user.password, password): + return jsonify({"msg": "Usuario o contraseña incorrectos"}), 401 + token = jwt.encode({ + 'user_id': user.id, + 'exp': datetime.now(timezone.utc) + timedelta(minutes=15) + }, SECRET_KEY, algorithm="HS256") + return jsonify({"token": token, "user": user.serialize()}) + + @api_user.route('//perfil', methods=['GET']) def get_perfil(user_id): - varPerfil = Perfil.query.filter_by(user_id=user_id).first() + varPerfil = User.query.filter_by(user_id=user_id).first() if varPerfil is None: return jsonify({"msg": f"El usuario con el ID {user_id} no existe"}), 404 - + response_body = { "Perfil": varPerfil.serialize() } return jsonify(response_body), 200 + @api_user.route('//perfil', methods=['PUT']) def editar_perfil(user_id): varUser = User.query.get(user_id) if varUser is None: return jsonify({'msg': f'El usuario con ID {user_id} no existe'}), 404 - varPerfil = Perfil.query.filter_by(user_id=user_id).first() + varPerfil = User.query.filter_by(user_id=user_id).first() if varPerfil is None: return jsonify({'msg': f'El usuario con ID {user_id} no tiene perfil creado'}), 404 body = request.get_json(silent=True) @@ -99,6 +118,7 @@ def editar_perfil(user_id): 'msg': 'Perfil actualizado correctamente', 'perfil': varPerfil.serialize() }), 200 + @api_user.route('/Saluda', methods=['POST', 'GET']) def handle_hello(): diff --git a/src/front/components/Form.jsx b/src/front/components/Form.jsx index e7a7fb1711..700a83ebf5 100644 --- a/src/front/components/Form.jsx +++ b/src/front/components/Form.jsx @@ -1,14 +1,18 @@ import React, { useState, useEffect } from "react"; import "../styles/Form.css"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; + + const Form = ({ mode, onSubmit, successMessage, userData }) => { const [email, setEmail] = useState(userData?.email || ""); + const [name, setName] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [newEmail, setNewEmail] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmNewPassword, setConfirmNewPassword] = useState(""); const [errorMsn, setErrorMsn] = useState(null); + const location = useLocation() useEffect(() => { if (successMessage) setErrorMsn(successMessage); @@ -35,11 +39,19 @@ const Form = ({ mode, onSubmit, successMessage, userData }) => { setConfirmPassword(""); setNewPassword(""); setConfirmNewPassword(""); + setName("") } else { - await onSubmit({ email, password, setErrorMsn }); + if (mode === "register") { + await onSubmit({ email, password, name, setErrorMsn }); + + } else { + await onSubmit({ email, password, setErrorMsn }); + + } setEmail(""); setPassword(""); setConfirmPassword(""); + setName("") } } catch (error) { setErrorMsn(error.message || "Error"); @@ -47,58 +59,143 @@ const Form = ({ mode, onSubmit, successMessage, userData }) => { }; return ( -
-
-
- Visual de login -
-
- -
- -

- {mode === "register" ? "Registrarse" : "Iniciar Sesión"} -

- -
- -
- - setEmail(e.target.value)} - required - /> +
+
+ {mode !== "config" && ( +
+ Visual +
+ )} +
+
+

+ {mode === "register" && "Registrarse"} + {mode === "login" && "Iniciar Sesión"} + {mode === "config" && "Configuración"} +

+ {errorMsn && ( +
+ {errorMsn}
+ )} + + { + mode === "register" && ( +
+ + setName(e.target.value)} + required + /> +
+ ) + } + {(mode === "register" || mode === "login") && ( + <> +
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {mode === "register" && ( +
+ + setConfirmPassword(e.target.value)} + required + /> +
+ )} + + )} + {mode === "config" && ( + <> +
+ + +
+
+ + setNewEmail(e.target.value)} + /> +
+
+ + setNewPassword(e.target.value)} + /> + setConfirmNewPassword(e.target.value)} + /> +
+ + )} + + { + mode === "register" ? ( +

+ ¿Ya tienes cuenta? + Inicia sesión +

+ ) : ( +

-

- - setPassword(e.target.value)} - required - /> -
+ ¿No tienes cuenta? + Registrate +

+ ) + } - {errorMsn && ( -
- {errorMsn} -
+ {mode === "config" && ( + + Cancelar + )} - -
diff --git a/src/front/pages/Dashboard.jsx b/src/front/pages/Dashboard.jsx index 1bde9b1aac..36501366f6 100644 --- a/src/front/pages/Dashboard.jsx +++ b/src/front/pages/Dashboard.jsx @@ -156,7 +156,7 @@ export const Dashboard = () => { {/* --- NUEVO MODAL PARA AÑADIR TAREAS --- */} { - (showTaskModal && taskType === "user") && + (showTaskModal && taskType === "user") && } {showTaskModal && taskType !== "user" && (
@@ -223,9 +223,14 @@ export const Dashboard = () => {

Bienvenido de nuevo '{store.profile.name}'

- + + + + +
diff --git a/src/front/pages/Login.jsx b/src/front/pages/Login.jsx index 9a47b5e871..917cf0b155 100644 --- a/src/front/pages/Login.jsx +++ b/src/front/pages/Login.jsx @@ -1,18 +1,20 @@ import React from "react"; import { useNavigate, useLocation } from "react-router-dom"; import Form from "../components/Form.jsx"; +import useGlobalReducer from "../hooks/useGlobalReducer.jsx"; const Login = () => { const navigate = useNavigate(); const location = useLocation(); const successMessage = location.state?.successMessage; + const { store, dispatch } = useGlobalReducer(); const handleLogin = async ({ email, password, setErrorMsn }) => { setErrorMsn(null); const backendUrl = import.meta.env.VITE_BACKEND_URL; try { - const response = await fetch(`${backendUrl}/api/login`, { + const response = await fetch(`${backendUrl}api/users/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), @@ -27,6 +29,8 @@ const Login = () => { } localStorage.setItem("token", data.token); + dispatch({ type: "SET_USER_INFO", payload: data.user }); + navigate("/dashboard", { state: { successMessage: "Inicio de sesión exitoso" }, diff --git a/src/front/pages/Register.jsx b/src/front/pages/Register.jsx index b6a209833e..7ae707869e 100644 --- a/src/front/pages/Register.jsx +++ b/src/front/pages/Register.jsx @@ -5,15 +5,15 @@ import Form from "../components/Form.jsx"; const Register = () => { const navigate = useNavigate(); - const handleSignup = async ({ email, password, setErrorMsn }) => { + const handleSignup = async ({ email, password, name, setErrorMsn }) => { setErrorMsn(null); const backendUrl = import.meta.env.VITE_BACKEND_URL; try { - const response = await fetch(`${backendUrl}/api/register`, { + const response = await fetch(`${backendUrl}/api/users/register`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), + body: JSON.stringify({ email, password, name }), }); const data = await response.json(); diff --git a/src/front/store.js b/src/front/store.js index 23ea33c1f2..d737bc3bcb 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -143,8 +143,8 @@ export const initialStore = () => { // Reducer que maneja todas las acciones export default function storeReducer(store, action = {}) { switch (action.type) { - case "set_hello": - return { ...store, message: action.payload }; + case "SET_USER_INFO": + return { ...store, profile: action.payload }; case "ADD_USER_TASK": const newUserTask = { diff --git a/src/front/styles/Form.css b/src/front/styles/Form.css index 5cc34d3bb7..d21ac12618 100644 --- a/src/front/styles/Form.css +++ b/src/front/styles/Form.css @@ -11,6 +11,22 @@ justify-content: center; } +.Form-login { + width: 100%; + max-width: 600px; + min-height: 60vh; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 16px; + padding: 60px 40px; + box-shadow: 0 10px 32px rgba(0, 0, 0, 0.50); + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; +} + + .button { background-color: #6366f1; color: white; From 0b75b913040f15e9cb67451ece07555980365c78 Mon Sep 17 00:00:00 2001 From: Mnrubn <126007823+Mnrubn@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:43:54 +0000 Subject: [PATCH 37/54] commit ruben --- src/app.py | 11 +---------- src/front/components/Form.jsx | 23 +++++++++-------------- src/front/pages/Config.jsx | 3 +-- src/front/routes.jsx | 3 ++- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/app.py b/src/app.py index 34d835428e..2bdee169f7 100644 --- a/src/app.py +++ b/src/app.py @@ -10,17 +10,14 @@ from api.routes import api from api.admin import setup_admin from api.commands import setup_commands -from flask_cors import CORS # from models import Person 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__) app.url_map.strict_slashes = False -CORS(app, resources={r"/*": {"origins": "*"}}) # database condiguration db_url = os.getenv("DATABASE_URL") @@ -72,10 +69,4 @@ def serve_any_other_file(path): # this only runs if `$ python src/main.py` is executed if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) - -from api.routesTasks import api_tasks -app.register_blueprint(api_tasks, url_prefix='/api/users') - -from api.routesUser import api_user -app.register_blueprint(api_user, url_prefix='/api/users') \ No newline at end of file + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/components/Form.jsx b/src/front/components/Form.jsx index 700a83ebf5..998600087c 100644 --- a/src/front/components/Form.jsx +++ b/src/front/components/Form.jsx @@ -176,20 +176,15 @@ const Form = ({ mode, onSubmit, successMessage, userData }) => { {mode === "login" && "Iniciar Sesión"} {mode === "config" && "Guardar Cambios"} - { - mode === "register" ? ( -

- ¿Ya tienes cuenta? - Inicia sesión -

- ) : ( -

- - ¿No tienes cuenta? - Registrate -

- ) - } + {mode === "register" ? ( +

+ ¿Ya tienes cuenta? Inicia sesión +

+ ) : mode === "login" ? ( +

+ ¿No tienes cuenta? Regístrate +

+ ) : null} {mode === "config" && ( diff --git a/src/front/pages/Config.jsx b/src/front/pages/Config.jsx index 20e9bde72b..fdc8e5af3f 100644 --- a/src/front/pages/Config.jsx +++ b/src/front/pages/Config.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import Form from "../components/Form.jsx"; -const Config = () => { +export const Config = () => { const backendUrl = import.meta.env.VITE_BACKEND_URL; const [userData, setUserData] = useState({ email: "", password: "********" }); const [errorMsn, setErrorMsn] = useState(null); @@ -72,4 +72,3 @@ const Config = () => { ); }; -export default Config; \ No newline at end of file diff --git a/src/front/routes.jsx b/src/front/routes.jsx index c2f814bd30..ebb7b5bcc0 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -15,7 +15,7 @@ import { Profile } from "./pages/Profile"; import { Groups } from "./pages/Groups"; import { TaskUser } from "./pages/TaskUser"; import { Finances } from "./pages/Finances"; - +import { Config } from "./pages/Config" export const router = createBrowserRouter( createRoutesFromElements( // CreateRoutesFromElements function allows you to build route elements declaratively. @@ -38,6 +38,7 @@ export const router = createBrowserRouter( } /> } /> } /> + } /> ) ); \ No newline at end of file From 6829fc019357a411002bd2f4c4818f9f9cfd6656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfonso=20Mu=C3=B1oz=20=20Ttitua=C3=B1a?= Date: Thu, 20 Nov 2025 17:49:06 +0000 Subject: [PATCH 38/54] cambio models.py agrego task --- src/api/models.py | 43 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/src/api/models.py b/src/api/models.py index f14043cb6a..f336db1e97 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -47,43 +47,26 @@ def serialize(self): } -class Tareas(db.Model): - __tablename__ = 'tareas' +class Task(db.Model): + __tablename__ = 'Task' id: Mapped[int] = mapped_column(Integer, primary_key=True) - titulo: Mapped[str] = mapped_column(String(80), nullable=False) - evento_id: Mapped[int] = mapped_column( - ForeignKey('evento.id'), nullable=True) - db_evento_tareas: Mapped['Evento'] = relationship( - back_populates='db_tareas_evento') - fecha: Mapped[datetime] = mapped_column(DateTime, nullable=True) - descripcion: Mapped[str] = mapped_column(String(250), nullable=True) - prioridad_id: Mapped[int] = mapped_column( - ForeignKey('prioridad.id'), nullable=True) - db_prioridad_tareas: Mapped['Prioridad'] = relationship( - back_populates='db_tareas_prioridad') - estado_id: Mapped[int] = mapped_column( - ForeignKey('estado.id'), nullable=True) - db_estado_tareas: Mapped['Estado'] = relationship( - back_populates='db_tareas_estado') - imagen: Mapped[str] = mapped_column(String(255), nullable=True) - db_mision_tareas: Mapped[list['Mision']] = relationship( - back_populates='db_tareas_mision') - db_tareas_asignadas_tareas: Mapped[list['TareasAsignadas']] = relationship( - back_populates='db_tareas_tareas_asignadas') + title: Mapped[str] = mapped_column(String(80), nullable=False) + date: Mapped[datetime] = mapped_column(DateTime, nullable=True) + description: Mapped[str] = mapped_column(String(250), nullable=True) + lat: Mapped[str] = mapped_column(String(255), nullable=True) + lng: Mapped[str] = mapped_column(String(255), nullable=True) def __repr__(self): - return f'{self.titulo}' + return f'{self.title}' def serialize(self): return { "id": self.id, - "titulo": self.titulo, - "imagen": self.imagen, - "descripcion": self.descripcion, - "fecha": self.fecha, - "evento_id": self.evento_id, - "prioridad_id": self.prioridad_id, - "estado_id": self.estado_id, + "title": self.title, + "date": self.date, + "description": self.description, + "lat": self.lat, + "lng": self.lng } From 4457d0c20915ad2a3bcea61ac0866c52012ea12a Mon Sep 17 00:00:00 2001 From: Mnrubn <126007823+Mnrubn@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:51:32 +0000 Subject: [PATCH 39/54] Landing --- src/front/pages/ LandingPage.jsx | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/front/pages/ LandingPage.jsx diff --git a/src/front/pages/ LandingPage.jsx b/src/front/pages/ LandingPage.jsx new file mode 100644 index 0000000000..c06ef7af42 --- /dev/null +++ b/src/front/pages/ LandingPage.jsx @@ -0,0 +1,40 @@ +import React from "react"; +import { motion } from "framer-motion"; +import LandingCards from "../components/LandingCards"; +import "../styles/Landing.css"; + +export function LandingPage() { + return ( +
+
+ + Bienvenido a TaskFlow + + + Tu herramienta para gestionar tareas y comunicación de equipos de manera simple y eficiente. + +
+ + + +
+ + ¿Por qué TaskFlow? + + + TaskFlow te permite organizar tus tareas, comunicarte con tu equipo y mantener un flujo de trabajo eficiente. + Todo en un solo lugar, accesible desde cualquier dispositivo. + +
+ +
+ window.location.href = "/register"}> + ¡Empieza ahora! + +
+
+ ); +} + + + From 8a146c017d7d340ec6a8e5078509892a53c4e349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfonso=20Mu=C3=B1oz=20=20Ttitua=C3=B1a?= Date: Thu, 20 Nov 2025 19:09:51 +0000 Subject: [PATCH 40/54] Cambios en dashboard. landyn enrrutada. --- src/front/pages/ LandingPage.jsx | 40 -------------------- src/front/pages/Dashboard.jsx | 63 +++++++++++++------------------- src/front/pages/Home.jsx | 7 ++++ src/front/routes.jsx | 6 ++- src/front/store.js | 2 +- 5 files changed, 37 insertions(+), 81 deletions(-) delete mode 100644 src/front/pages/ LandingPage.jsx diff --git a/src/front/pages/ LandingPage.jsx b/src/front/pages/ LandingPage.jsx deleted file mode 100644 index c06ef7af42..0000000000 --- a/src/front/pages/ LandingPage.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import { motion } from "framer-motion"; -import LandingCards from "../components/LandingCards"; -import "../styles/Landing.css"; - -export function LandingPage() { - return ( -
-
- - Bienvenido a TaskFlow - - - Tu herramienta para gestionar tareas y comunicación de equipos de manera simple y eficiente. - -
- - - -
- - ¿Por qué TaskFlow? - - - TaskFlow te permite organizar tus tareas, comunicarte con tu equipo y mantener un flujo de trabajo eficiente. - Todo en un solo lugar, accesible desde cualquier dispositivo. - -
- -
- window.location.href = "/register"}> - ¡Empieza ahora! - -
-
- ); -} - - - diff --git a/src/front/pages/Dashboard.jsx b/src/front/pages/Dashboard.jsx index 36501366f6..728f60564d 100644 --- a/src/front/pages/Dashboard.jsx +++ b/src/front/pages/Dashboard.jsx @@ -223,12 +223,7 @@ export const Dashboard = () => {

Bienvenido de nuevo '{store.profile.name}'

- - - +
@@ -260,33 +255,7 @@ export const Dashboard = () => {
- {/* --- TARJETA DE FINANZAS UNIFICADA --- */} -
-
-

Resumen Financiero

-
- {/* Saldo Bote Personal */} -
-

Saldo del Bote

-
- -

{store.personalBote.toFixed(2)}€

-
-
- {/* Gastos del Mes (Total) */} -
-

Gastos del Mes

-
- -

{totalExpenses.toFixed(2)}€

-
-
-
-
-
- {/* --- FIN DE TARJETA UNIFICADA --- */} - - {/* Tareas de Clanes */} + {/* Tareas de Clanes */}
@@ -314,7 +283,6 @@ export const Dashboard = () => {
- {/* Tareas Completadas */}
@@ -328,14 +296,33 @@ export const Dashboard = () => {
- {/* Próximos Eventos */} -
+ {/* --- TARJETA DE FINANZAS UNIFICADA --- */} +
-

Próximos Eventos

-

No hay eventos próximos.

+

Resumen Financiero

+
+ {/* Saldo Bote Personal */} +
+

Saldo del Bote

+
+ +

{store.personalBote.toFixed(2)}€

+
+
+ {/* Gastos del Mes (Total) */} +
+

Gastos del Mes

+
+ +

{totalExpenses.toFixed(2)}€

+
+
+
+ {/* --- FIN DE TARJETA UNIFICADA --- */} + {/* Mensajes (Placeholder) */}
diff --git a/src/front/pages/Home.jsx b/src/front/pages/Home.jsx index 23146fb076..a13920e9ca 100644 --- a/src/front/pages/Home.jsx +++ b/src/front/pages/Home.jsx @@ -31,6 +31,13 @@ export default function Home() { window.location.href = "/register"}> ¡Empieza ahora! + + + setShowInviteModal(true)}> + + Login + +
); diff --git a/src/front/routes.jsx b/src/front/routes.jsx index ebb7b5bcc0..82daa29f72 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -16,6 +16,7 @@ import { Groups } from "./pages/Groups"; import { TaskUser } from "./pages/TaskUser"; import { Finances } from "./pages/Finances"; import { Config } from "./pages/Config" +import Home from "./pages/Home"; export const router = createBrowserRouter( createRoutesFromElements( // CreateRoutesFromElements function allows you to build route elements declaratively. @@ -26,8 +27,8 @@ export const router = createBrowserRouter( // Root Route: All navigation will start from here. } errorElement={

Not found!

} > - } /> - } /> {/* Ruta explícita */} + } /> + } /> {/* Ruta explícita */} {/* Nested Routes: Defines sub-routes within the BaseHome component. */} } /> {/* Dynamic route for single items */} @@ -39,6 +40,7 @@ export const router = createBrowserRouter( } /> } /> } /> + } /> ) ); \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index d737bc3bcb..4e1ddd37cf 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -91,7 +91,7 @@ export const initialStore = () => { 3: 200.0, }, - // 🔥 Recuperado del revert + // Recuperado del revert token: localStorage.getItem("token") || null, // Finanzas From 654a3052ff3d865554925ccd056ed4f86e06a41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfonso=20Mu=C3=B1oz=20=20Ttitua=C3=B1a?= Date: Fri, 21 Nov 2025 18:23:18 +0000 Subject: [PATCH 41/54] cambios 21/11/2025 --- src/front/components/ModalCreateTask.jsx | 193 +++++++---- src/front/pages/Chat.jsx | 166 +++++++++ src/front/pages/Dashboard.jsx | 234 +++---------- src/front/pages/Groups.jsx | 68 ++-- src/front/pages/Home.jsx | 7 - src/front/pages/Profile.jsx | 84 ++--- src/front/pages/SharedTasks.jsx | 156 +++++++++ src/front/pages/TaskUser.jsx | 129 ++++--- src/front/routes.jsx | 4 + src/front/store.js | 420 ++++++++++------------- 10 files changed, 813 insertions(+), 648 deletions(-) create mode 100644 src/front/pages/Chat.jsx create mode 100644 src/front/pages/SharedTasks.jsx diff --git a/src/front/components/ModalCreateTask.jsx b/src/front/components/ModalCreateTask.jsx index 1f8216dad6..33245fbf14 100644 --- a/src/front/components/ModalCreateTask.jsx +++ b/src/front/components/ModalCreateTask.jsx @@ -1,94 +1,141 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import useGlobalReducer from "../hooks/useGlobalReducer"; import GoogleMaps from "../components/GoogleMaps"; -function ModalCreateTask({ setShowTaskModal }) { - const { dispatch } = useGlobalReducer(); +function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { + const { store, dispatch } = useGlobalReducer(); + const activeClanId = store.activeClanId; + + const isEditing = !!taskToEdit; + const modalTitle = isEditing + ? (taskType === 'user' ? "Editar Tarea Personal" : "Editar Tarea de Clan") + : (taskType === 'user' ? "Nueva Tarea Personal" : "Nueva Tarea de Clan"); + + const buttonColor = taskType === 'user' ? "btn-custom-blue" : "btn-custom-purple"; + const buttonText = isEditing ? "Guardar Cambios" : "Crear Tarea"; + + // Estados const [titulo, setTitulo] = useState(""); const [descripcion, setDescripcion] = useState(""); const [direccion, setDireccion] = useState(""); - const [lat, setLat] = useState(""); - const [lng, setLng] = useState(""); + const [lat, setLat] = useState(20); + const [lng, setLng] = useState(-99); const [msg, setMsg] = useState(""); + // EFECTO: Rellena el formulario si estamos editando + useEffect(() => { + if (taskToEdit) { + setTitulo(taskToEdit.title || ""); + setDescripcion(taskToEdit.description || ""); + setDireccion(taskToEdit.address || ""); + setLat(taskToEdit.latitude || 20); + setLng(taskToEdit.longitude || -99); + } else { + // Limpiar si es creación nueva + setTitulo(""); + setDescripcion(""); + setDireccion(""); + } + }, [taskToEdit]); + const handleSubmit = (e) => { e.preventDefault(); + setMsg(""); - dispatch({ type: 'ADD_USER_TASK', payload: { title: newTaskTitle } }); - - setTitulo(""); - setDescripcion(""); - setDireccion(""); - setLat(""); - setLng(""); - setMsg("Tarea creada (mock)"); - - }; + if (!titulo.trim()) { + setMsg("El título es obligatorio."); + return; + } - const handleInvitadosChange = (e) => { - let value = e.target.value; + const payloadData = { + id: taskToEdit ? taskToEdit.id : undefined, // Importante para editar + title: titulo, + description: descripcion, + address: direccion, + latitude: lat, + longitude: lng, + }; - const emails = value.split(',').map(i => i.trim()).filter(i => i); - if (emails.length > 1 && !value.trim().endsWith(',')) { - value = value + ', '; + if (taskType === 'clan') { + if (!isEditing && !activeClanId) { + setMsg("Error: No hay clan activo."); + return; + } + // Dispatch específico para CLAN (Crear o Editar) + dispatch({ + type: isEditing ? 'UPDATE_CLAN_TASK' : 'ADD_TASK_TO_CLAN', + payload: { ...payloadData, clanId: activeClanId } + }); + } else { + // Dispatch específico para USER (Crear o Editar) + dispatch({ + type: isEditing ? 'UPDATE_USER_TASK' : 'ADD_USER_TASK', + payload: payloadData + }); } - setInvitados(value); + + setShowTaskModal(false); }; return ( -
-
-
-
- Añadir Tarea de Clan -
- -
- setTitulo(e.target.value)} style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} /> -
-
+
+
+
+
-
-
-
+
+
+

Redes
-
-
-
+
+
+
@@ -108,7 +152,6 @@ export const Profile = () => {
)} - {/* --- MODAL PARA EDITAR BOTE --- */} {showBoteModal && (
@@ -121,15 +164,7 @@ export const Profile = () => {
- setBoteAmount(e.target.value)} - placeholder="0" - /> + setBoteAmount(e.target.value)} placeholder="0" />
@@ -142,103 +177,79 @@ export const Profile = () => {
)} - {/* --- CONTENIDO DE LA PÁGINA --- */}
- {/* --- COLUMNA IZQUIERDA (Perfil + Amigos) --- */}
- Foto de perfil + Foto de perfil

{store.profile.name}

{store.profile.email}

- +
+ +
Amigos activos
- {store.friends.map(friend => ( - - ))} + {store.friends.map(friend => ())}
- {/* --- COLUMNA DERECHA --- */}
- {/* BARRA DE RESUMEN */}
{tasksCompleted}
Tareas completadas
{tasksPending}
Tareas sin hacer
{clanCount}
Clan
- {/* --- CUADRÍCULA 2x2 --- */}
- {/* Detalles */}

Detalles

-

{store.profile.presentation}

+

{store.profile.bio}


-

{store.profile.location}

+

{store.profile.city}

{store.profile.age} años

{store.profile.phone}

{store.profile.gender}

- {/* Mensajes */} +

Mensajes

No hay mensajes nuevos.

- {/* Otras Redes */} +

Otras redes

-

{store.profile.social.instagram}

-

{store.profile.social.twitter}

-

{store.profile.social.facebook}

+

{store.profile.instagram || ""}

+

{store.profile.twitter || ""}

+

{store.profile.facebook || ""}

- - {/* --- NUEVA TARJETA: GASTOS DEL MES --- */} +

Gastos del Mes

{totalExpenses.toFixed(2)}€

- - Ver Detalles - + Ver Detalles
-
- {/* --- BOTE PERSONAL --- */}

Saldo del Bote Personal

-
-

- {store.personalBote.toFixed(2)} € -

+

{store.personalBote.toFixed(2)} €

diff --git a/src/front/routes.jsx b/src/front/routes.jsx index d6ae6681f4..d95ed445ce 100644 --- a/src/front/routes.jsx +++ b/src/front/routes.jsx @@ -5,6 +5,7 @@ import { createRoutesFromElements, Route, } from "react-router-dom"; +import PrivateRoute from "./components/PrivateRoute"; import { Layout } from "./pages/Layout"; import { Dashboard } from "./pages/Dashboard"; import { Single } from "./pages/Single"; @@ -27,24 +28,80 @@ export const router = createBrowserRouter( // Note: keep in mind that errorElement will be the default page when you don't get a route, customize that page to make your project more attractive. // Note: The child paths of the Layout element replace the Outlet component with the elements contained in the "element" attribute of these child paths. - // Root Route: All navigation will start from here. - } errorElement={

Not found!

} > - } /> - } /> {/* Ruta explícita */} - {/* Nested Routes: Defines sub-routes within the BaseHome component. */} - - } /> {/* Dynamic route for single items */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - ) + // Root Route: All navigation will start from here. + } errorElement={

Not found!

} > + } /> + } /> {/* Ruta explícita */} + {/* Nested Routes: Defines sub-routes within the BaseHome component. */} + + } /> {/* Dynamic route for single items */} + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + ) ); \ No newline at end of file diff --git a/src/front/store.js b/src/front/store.js index 64ce3d6fec..06c1a2ed43 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,36 +1,30 @@ export const initialStore = () => { + const savedProfile = localStorage.getItem("profile"); return { - // --- AUTENTICACIÓN --- token: localStorage.getItem("token") || null, - user: null, //{ id, email } real del usuario - - // --- PERFIL DE USUARIO --- - profile: { - name: "Nombre de Usuario", - email: "correo@delusuario.com", - avatar: "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png", - presentation: "Hola! Soy nuevo en TaskFlow.", - location: "Madrid, ES", - age: 26, - phone: "666 000 666", - gender: "Sin especificar", - social: { instagram: "@usuario", twitter: "@usuario", facebook: "/usuario" }, - }, - - // --- SOCIAL --- + user: JSON.parse(localStorage.getItem("user")) || null, + profile: savedProfile + ? JSON.parse(savedProfile) + : { + name: "Nombre de Usuario", + email: "correo@delusuario.com", + avatar: "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png", + presentation: "Hola! Soy nuevo en TaskFlow.", + location: "Madrid, ES", + age: 26, + phone: "666 000 666", + gender: "Sin especificar", + social: { instagram: "", twitter: "", facebook: "" }, + }, friends: [ { id: 1, name: "Amigo Uno", status: "online", avatar: "https://i.pravatar.cc/150?img=11" }, { id: 2, name: "Amiga Dos", status: "offline", avatar: "https://i.pravatar.cc/150?img=12" }, ], - - // --- ESTRUCTURA DE CLANES --- clans: [ { id: 1, name: "Los geek's", category: "Trabajo", members: 4, created: "2025-11-03" }, { id: 2, name: "Familia", category: "Familia", members: 3, created: "2025-10-01" }, ], - activeClanId: 1, // El clan que estás viendo actualmente - - // --- TAREAS PERSONALES --- + activeClanId: 1, userTasks: [ { id: 1, @@ -42,54 +36,54 @@ export const initialStore = () => { completed: false }, ], - - // --- TAREAS DE CLAN --- clanTasks: [ { id: 101, clanId: 1, title: "Deploy a producción", description: "Subir cambios al servidor", date: "2025-11-22", time: "14:00", completed: false }, { id: 102, clanId: 2, title: "Comprar regalo mamá", description: "Cumpleaños es el domingo", date: "2025-11-25", time: "18:00", completed: false }, ], - - // --- FINANZAS --- - personalBote: 119.58, // Tu dinero personal - personalExpenses: [ // Tus gastos personales - { id: 1, concept: "Café", amount: 1.5, date: "2025-11-20" } + personalBote: 119.58, + personalExpenses: [ + { id: 1, concept: "Café", amount: 1.5, date: "2025-11-20" } ], - expenses: [], // Gastos del clan - commonBote: { // Dinero de cada clan (ID Clan: Cantidad) - 1: 150.00, - 2: 50.00 - }, - balances: [], // Deudas entre miembros - - // --- CHAT --- + expenses: [], + commonBote: { 1: 150.00, 2: 50.00 }, + balances: [], chatMessages: [ - { id: 1, clanId: 1, userId: 2, userName: "Amigo Uno", text: "Hola equipo", time: "10:00", isMe: false }, - { id: 2, clanId: 1, userId: 99, userName: "Yo", text: "¡Hola! Listos para trabajar", time: "10:05", isMe: true }, - { id: 3, clanId: 2, userId: 99, userName: "Yo", text: "¿Quién compra la cena?", time: "20:00", isMe: true }, + { id: 1, clanId: 1, userId: 2, userName: "Amigo Uno", text: "Hola equipo", time: "10:00", isMe: false }, + { id: 2, clanId: 1, userId: 99, userName: "Yo", text: "¡Hola! Listos para trabajar", time: "10:05", isMe: true }, + { id: 3, clanId: 2, userId: 99, userName: "Yo", text: "¿Quién compra la cena?", time: "20:00", isMe: true }, ] }; }; export default function storeReducer(store, action = {}) { switch (action.type) { - case "LOAD_DATA_FROM_BACKEND": - return { - ...store, - // Sobreescribimos los datos iniciales con los que vienen de la API - user: action.payload.user, - profile: action.payload.profile || store.profile, - userTasks: action.payload.userTasks || [], - clans: action.payload.clans || [], - clanTasks: action.payload.clanTasks || [], - // Mantenemos la estructura financiera si el back no la trae aún - }; - - - // --- TAREAS PERSONALES --- + const profile = { + ...store.profile, + ...action.payload.profile, + social: action.payload.profile?.social || store.profile.social || { instagram: "", twitter: "", facebook: "" } + }; + localStorage.setItem("user", JSON.stringify(action.payload.user || store.user)); + localStorage.setItem("profile", JSON.stringify(profile)); + return { + ...store, + user: action.payload.user || store.user, + profile, + userTasks: action.payload.userTasks || [], + clans: action.payload.clans || [], + clanTasks: action.payload.clanTasks || [], + }; + case "SET_TOKEN": + localStorage.setItem("token", action.payload.token); + return { ...store, token: action.payload.token }; + case "LOGOUT": + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("profile"); + return { ...store, token: null, user: null }; case "ADD_USER_TASK": const newUserTask = { - id: new Date().getTime(), // ID temporal (el back dará el real luego) + id: new Date().getTime(), title: action.payload.title, description: action.payload.description || "", address: action.payload.address || "Ubicación no especificada", @@ -101,28 +95,12 @@ export default function storeReducer(store, action = {}) { completed: false, }; return { ...store, userTasks: [...store.userTasks, newUserTask] }; - case "UPDATE_USER_TASK": - return { - ...store, - userTasks: store.userTasks.map((task) => - task.id === action.payload.id ? { ...task, ...action.payload } : task - ), - }; - + return { ...store, userTasks: store.userTasks.map((task) => task.id === action.payload.id ? { ...task, ...action.payload } : task) }; case "TOGGLE_USER_TASK": - return { - ...store, - userTasks: store.userTasks.map((task) => - task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task - ), - }; - + return { ...store, userTasks: store.userTasks.map((task) => task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task) }; case "DELETE_USER_TASK": return { ...store, userTasks: store.userTasks.filter((t) => t.id !== action.payload.taskId) }; - - - // --- TAREAS DE CLAN --- case "ADD_TASK_TO_CLAN": if (!store.activeClanId) return store; const newClanTask = { @@ -136,113 +114,46 @@ export default function storeReducer(store, action = {}) { completed: false, }; return { ...store, clanTasks: [...store.clanTasks, newClanTask] }; - case "UPDATE_CLAN_TASK": - return { - ...store, - clanTasks: store.clanTasks.map((task) => - task.id === action.payload.id ? { ...task, ...action.payload } : task - ), - }; - + return { ...store, clanTasks: store.clanTasks.map((task) => task.id === action.payload.id ? { ...task, ...action.payload } : task) }; case "TOGGLE_CLAN_TASK": - return { - ...store, - clanTasks: store.clanTasks.map((task) => - task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task - ), - }; - + return { ...store, clanTasks: store.clanTasks.map((task) => task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task) }; case "DELETE_CLAN_TASK": return { ...store, clanTasks: store.clanTasks.filter((t) => t.id !== action.payload.taskId) }; - case "SET_ACTIVE_CLAN": return { ...store, activeClanId: action.payload.clanId }; - case "CREATE_CLAN": - const newClan = { - id: new Date().getTime(), - members: 1, - created: new Date().toISOString().split('T')[0], - ...action.payload - }; + const newClan = { id: new Date().getTime(), members: 1, created: new Date().toISOString().split('T')[0], ...action.payload }; return { ...store, clans: [...store.clans, newClan] }; - case "DELETE_CLAN": if (!store.activeClanId) return store; const remainingClans = store.clans.filter(c => c.id !== store.activeClanId); - return { - ...store, - clans: remainingClans, - activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null - }; - + return { ...store, clans: remainingClans, activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null }; case "UPDATE_PROFILE": - return { ...store, profile: { ...store.profile, ...action.payload } }; - - + const updatedProfile = { + ...store.profile, + ...action.payload, + social: action.payload.social || store.profile.social || { instagram: "", twitter: "", facebook: "" } + }; + localStorage.setItem("profile", JSON.stringify(updatedProfile)); + return { ...store, profile: updatedProfile }; case "UPDATE_PERSONAL_BOTE": - return { ...store, personalBote: parseFloat(action.payload.newBote) }; - + return { ...store, personalBote: parseFloat(action.payload.newBote) }; case "ADD_PERSONAL_EXPENSE": - const newPExpense = { - id: new Date().getTime(), - concept: action.payload.concept, - amount: parseFloat(action.payload.amount), - date: new Date().toISOString().split("T")[0] - }; - return { - ...store, - personalBote: store.personalBote - newPExpense.amount, // Resta del bote - personalExpenses: [newPExpense, ...store.personalExpenses] - }; - - case "ADD_TO_BOTE": // Añadir dinero al bote común del clan - if (!store.activeClanId) return store; - const currentAmount = store.commonBote[store.activeClanId] || 0; - return { - ...store, - commonBote: { - ...store.commonBote, - [store.activeClanId]: currentAmount + parseFloat(action.payload.amount) - } - }; - - case "ADD_EXPENSE": // Gasto del clan - if (!store.activeClanId) return store; - const newCExpense = { - id: new Date().getTime(), - clanId: store.activeClanId, - concept: action.payload.concept, - amount: parseFloat(action.payload.amount), - paidBy: store.profile.name, - date: new Date().toISOString().split("T")[0] - }; - const currentBote = store.commonBote[store.activeClanId] || 0; - return { - ...store, - expenses: [newCExpense, ...store.expenses], - commonBote: { - ...store.commonBote, - [store.activeClanId]: currentBote - newCExpense.amount // Resta del bote común - } - }; - + const newPExpense = { id: new Date().getTime(), concept: action.payload.concept, amount: parseFloat(action.payload.amount), date: new Date().toISOString().split("T")[0] }; + return { ...store, personalBote: store.personalBote - newPExpense.amount, personalExpenses: [newPExpense, ...store.personalExpenses] }; + case "ADD_TO_BOTE": + if (!store.activeClanId) return store; + const currentAmount = store.commonBote[store.activeClanId] || 0; + return { ...store, commonBote: { ...store.commonBote, [store.activeClanId]: currentAmount + parseFloat(action.payload.amount) } }; + case "ADD_EXPENSE": + if (!store.activeClanId) return store; + const newCExpense = { id: new Date().getTime(), clanId: store.activeClanId, concept: action.payload.concept, amount: parseFloat(action.payload.amount), paidBy: store.profile.name, date: new Date().toISOString().split("T")[0] }; + const currentBote = store.commonBote[store.activeClanId] || 0; + return { ...store, expenses: [newCExpense, ...store.expenses], commonBote: { ...store.commonBote, [store.activeClanId]: currentBote - newCExpense.amount } }; case "SEND_MESSAGE": - const newMsg = { - id: new Date().getTime(), - clanId: store.activeClanId, - userId: store.user?.id || 99, - userName: store.profile.name, - text: action.payload.text, - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - isMe: true - }; + const newMsg = { id: new Date().getTime(), clanId: store.activeClanId, userId: store.user?.id || 99, userName: store.profile.name, text: action.payload.text, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), isMe: true }; return { ...store, chatMessages: [...store.chatMessages, newMsg] }; - - - - default: return store; } From efe75cdcaf445e2b0651514202c12ccb0f83742c Mon Sep 17 00:00:00 2001 From: Mnrubn <126007823+Mnrubn@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:44:34 +0000 Subject: [PATCH 46/54] store limpio --- src/front/store.js | 231 +++++++++++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 82 deletions(-) diff --git a/src/front/store.js b/src/front/store.js index 06c1a2ed43..df724f03f8 100644 --- a/src/front/store.js +++ b/src/front/store.js @@ -1,106 +1,98 @@ export const initialStore = () => { + const savedUser = localStorage.getItem("user"); const savedProfile = localStorage.getItem("profile"); + return { token: localStorage.getItem("token") || null, - user: JSON.parse(localStorage.getItem("user")) || null, - profile: savedProfile - ? JSON.parse(savedProfile) - : { - name: "Nombre de Usuario", - email: "correo@delusuario.com", - avatar: "https://res.cloudinary.com/dmx0zjkej/image/upload/v1762540958/LOGO_600_x_600_muoehy.png", - presentation: "Hola! Soy nuevo en TaskFlow.", - location: "Madrid, ES", - age: 26, - phone: "666 000 666", - gender: "Sin especificar", - social: { instagram: "", twitter: "", facebook: "" }, - }, - friends: [ - { id: 1, name: "Amigo Uno", status: "online", avatar: "https://i.pravatar.cc/150?img=11" }, - { id: 2, name: "Amiga Dos", status: "offline", avatar: "https://i.pravatar.cc/150?img=12" }, - ], - clans: [ - { id: 1, name: "Los geek's", category: "Trabajo", members: 4, created: "2025-11-03" }, - { id: 2, name: "Familia", category: "Familia", members: 3, created: "2025-10-01" }, - ], - activeClanId: 1, - userTasks: [ - { - id: 1, - title: "Revisar el correo", - description: "Responder a los clientes pendientes", - date: "2025-11-21", - time: "09:00", - address: "Oficina", - completed: false - }, - ], - clanTasks: [ - { id: 101, clanId: 1, title: "Deploy a producción", description: "Subir cambios al servidor", date: "2025-11-22", time: "14:00", completed: false }, - { id: 102, clanId: 2, title: "Comprar regalo mamá", description: "Cumpleaños es el domingo", date: "2025-11-25", time: "18:00", completed: false }, - ], - personalBote: 119.58, - personalExpenses: [ - { id: 1, concept: "Café", amount: 1.5, date: "2025-11-20" } - ], + user: savedUser ? JSON.parse(savedUser) : null, + profile: savedProfile ? JSON.parse(savedProfile) : null, + + friends: [], + clans: [], + activeClanId: null, + + userTasks: [], + clanTasks: [], + + personalBote: 0, + personalExpenses: [], + expenses: [], - commonBote: { 1: 150.00, 2: 50.00 }, + commonBote: {}, balances: [], - chatMessages: [ - { id: 1, clanId: 1, userId: 2, userName: "Amigo Uno", text: "Hola equipo", time: "10:00", isMe: false }, - { id: 2, clanId: 1, userId: 99, userName: "Yo", text: "¡Hola! Listos para trabajar", time: "10:05", isMe: true }, - { id: 3, clanId: 2, userId: 99, userName: "Yo", text: "¿Quién compra la cena?", time: "20:00", isMe: true }, - ] + + chatMessages: [] }; }; export default function storeReducer(store, action = {}) { switch (action.type) { + + case "RESET_STORE": + return initialStore(); + case "LOAD_DATA_FROM_BACKEND": - const profile = { - ...store.profile, - ...action.payload.profile, - social: action.payload.profile?.social || store.profile.social || { instagram: "", twitter: "", facebook: "" } - }; - localStorage.setItem("user", JSON.stringify(action.payload.user || store.user)); + const { user, profile, userTasks, clans, clanTasks } = action.payload; + + localStorage.setItem("user", JSON.stringify(user)); localStorage.setItem("profile", JSON.stringify(profile)); + return { ...store, - user: action.payload.user || store.user, + user, profile, - userTasks: action.payload.userTasks || [], - clans: action.payload.clans || [], - clanTasks: action.payload.clanTasks || [], + userTasks: userTasks || [], + clans: clans || [], + clanTasks: clanTasks || [] }; + case "SET_TOKEN": localStorage.setItem("token", action.payload.token); return { ...store, token: action.payload.token }; + case "LOGOUT": localStorage.removeItem("token"); localStorage.removeItem("user"); localStorage.removeItem("profile"); - return { ...store, token: null, user: null }; + return initialStore(); + case "ADD_USER_TASK": const newUserTask = { id: new Date().getTime(), title: action.payload.title, description: action.payload.description || "", - address: action.payload.address || "Ubicación no especificada", - date: new Date().toLocaleDateString(), - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + address: action.payload.address || "", + date: new Date().toISOString().split("T")[0], + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), latitude: action.payload.latitude, longitude: action.payload.longitude, - guests: [], - completed: false, + guests: [], + completed: false }; return { ...store, userTasks: [...store.userTasks, newUserTask] }; + case "UPDATE_USER_TASK": - return { ...store, userTasks: store.userTasks.map((task) => task.id === action.payload.id ? { ...task, ...action.payload } : task) }; + return { + ...store, + userTasks: store.userTasks.map(task => + task.id === action.payload.id ? { ...task, ...action.payload } : task + ) + }; + case "TOGGLE_USER_TASK": - return { ...store, userTasks: store.userTasks.map((task) => task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task) }; + return { + ...store, + userTasks: store.userTasks.map(task => + task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task + ) + }; + case "DELETE_USER_TASK": - return { ...store, userTasks: store.userTasks.filter((t) => t.id !== action.payload.taskId) }; + return { + ...store, + userTasks: store.userTasks.filter(t => t.id !== action.payload.taskId) + }; + case "ADD_TASK_TO_CLAN": if (!store.activeClanId) return store; const newClanTask = { @@ -109,51 +101,126 @@ export default function storeReducer(store, action = {}) { title: action.payload.title, description: action.payload.description || "", address: action.payload.address || "", - date: new Date().toLocaleDateString(), - time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), - completed: false, + date: new Date().toISOString().split("T")[0], + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + completed: false }; return { ...store, clanTasks: [...store.clanTasks, newClanTask] }; + case "UPDATE_CLAN_TASK": - return { ...store, clanTasks: store.clanTasks.map((task) => task.id === action.payload.id ? { ...task, ...action.payload } : task) }; + return { + ...store, + clanTasks: store.clanTasks.map(task => + task.id === action.payload.id ? { ...task, ...action.payload } : task + ) + }; + case "TOGGLE_CLAN_TASK": - return { ...store, clanTasks: store.clanTasks.map((task) => task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task) }; + return { + ...store, + clanTasks: store.clanTasks.map(task => + task.id === action.payload.taskId ? { ...task, completed: !task.completed } : task + ) + }; + case "DELETE_CLAN_TASK": - return { ...store, clanTasks: store.clanTasks.filter((t) => t.id !== action.payload.taskId) }; + return { + ...store, + clanTasks: store.clanTasks.filter(t => t.id !== action.payload.taskId) + }; + case "SET_ACTIVE_CLAN": return { ...store, activeClanId: action.payload.clanId }; + case "CREATE_CLAN": - const newClan = { id: new Date().getTime(), members: 1, created: new Date().toISOString().split('T')[0], ...action.payload }; + const newClan = { + id: new Date().getTime(), + members: 1, + created: new Date().toISOString().split("T")[0], + ...action.payload + }; return { ...store, clans: [...store.clans, newClan] }; + case "DELETE_CLAN": if (!store.activeClanId) return store; const remainingClans = store.clans.filter(c => c.id !== store.activeClanId); - return { ...store, clans: remainingClans, activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null }; + return { + ...store, + clans: remainingClans, + activeClanId: remainingClans.length > 0 ? remainingClans[0].id : null + }; + case "UPDATE_PROFILE": const updatedProfile = { ...store.profile, ...action.payload, - social: action.payload.social || store.profile.social || { instagram: "", twitter: "", facebook: "" } + social: + action.payload.social || + store.profile?.social || + { instagram: "", twitter: "", facebook: "" } }; localStorage.setItem("profile", JSON.stringify(updatedProfile)); return { ...store, profile: updatedProfile }; + case "UPDATE_PERSONAL_BOTE": return { ...store, personalBote: parseFloat(action.payload.newBote) }; + case "ADD_PERSONAL_EXPENSE": - const newPExpense = { id: new Date().getTime(), concept: action.payload.concept, amount: parseFloat(action.payload.amount), date: new Date().toISOString().split("T")[0] }; - return { ...store, personalBote: store.personalBote - newPExpense.amount, personalExpenses: [newPExpense, ...store.personalExpenses] }; + const newPExpense = { + id: new Date().getTime(), + concept: action.payload.concept, + amount: parseFloat(action.payload.amount), + date: new Date().toISOString().split("T")[0] + }; + return { + ...store, + personalBote: store.personalBote - newPExpense.amount, + personalExpenses: [newPExpense, ...store.personalExpenses] + }; + case "ADD_TO_BOTE": if (!store.activeClanId) return store; const currentAmount = store.commonBote[store.activeClanId] || 0; - return { ...store, commonBote: { ...store.commonBote, [store.activeClanId]: currentAmount + parseFloat(action.payload.amount) } }; + return { + ...store, + commonBote: { + ...store.commonBote, + [store.activeClanId]: currentAmount + parseFloat(action.payload.amount) + } + }; + case "ADD_EXPENSE": if (!store.activeClanId) return store; - const newCExpense = { id: new Date().getTime(), clanId: store.activeClanId, concept: action.payload.concept, amount: parseFloat(action.payload.amount), paidBy: store.profile.name, date: new Date().toISOString().split("T")[0] }; + const newCExpense = { + id: new Date().getTime(), + clanId: store.activeClanId, + concept: action.payload.concept, + amount: parseFloat(action.payload.amount), + paidBy: store.profile?.name, + date: new Date().toISOString().split("T")[0] + }; const currentBote = store.commonBote[store.activeClanId] || 0; - return { ...store, expenses: [newCExpense, ...store.expenses], commonBote: { ...store.commonBote, [store.activeClanId]: currentBote - newCExpense.amount } }; + return { + ...store, + expenses: [newCExpense, ...store.expenses], + commonBote: { + ...store.commonBote, + [store.activeClanId]: currentBote - newCExpense.amount + } + }; + case "SEND_MESSAGE": - const newMsg = { id: new Date().getTime(), clanId: store.activeClanId, userId: store.user?.id || 99, userName: store.profile.name, text: action.payload.text, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), isMe: true }; + const newMsg = { + id: new Date().getTime(), + clanId: store.activeClanId, + userId: store.user?.id, + userName: store.profile?.name, + text: action.payload.text, + time: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + isMe: true + }; return { ...store, chatMessages: [...store.chatMessages, newMsg] }; + default: return store; } From 23c6fe25054256ae8b1bfdd073cbfe2a6910f806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfonso=20Mu=C3=B1oz=20=20Ttitua=C3=B1a?= Date: Tue, 25 Nov 2025 15:47:18 +0000 Subject: [PATCH 47/54] Cambios en routesUser.jsx --- src/api/routesUser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/routesUser.py b/src/api/routesUser.py index c11bd88271..61e4768816 100644 --- a/src/api/routesUser.py +++ b/src/api/routesUser.py @@ -3,10 +3,11 @@ from api.models import db, User from werkzeug.security import generate_password_hash, check_password_hash import jwt +from flask_cors import CORS api_user = Blueprint('apiUser', __name__) SECRET_KEY = "super-secret-key" - +CORS (api_user) def token_requerido(f): def wrapper(*args, **kwargs): if request.method == 'OPTIONS': @@ -25,6 +26,7 @@ def wrapper(*args, **kwargs): @api_user.route('/register', methods=['POST']) def create_profile(): + print ("hola") body = request.get_json() email = body.get('email') password = body.get('password') @@ -136,4 +138,4 @@ def handle_hello(): response_body = { "message": "Este ya es el endpoint de Los usuarios Osea de cada user de la tabla" } - return jsonify(response_body), 200 + return jsonify(response_body), 200 \ No newline at end of file From 681c0775dc2b7470a87b691d488571ac9ba03c7c Mon Sep 17 00:00:00 2001 From: Nika254 Date: Tue, 25 Nov 2025 17:01:11 +0000 Subject: [PATCH 48/54] maps y modal --- src/app.py | 9 +-- src/front/components/GoogleMaps.jsx | 66 ++++++++++++++++++-- src/front/components/ModalCreateTask.jsx | 77 +++++++++++++++++------- src/front/components/TaskDetailModal.jsx | 38 ++++++++++++ src/front/pages/Dashboard.jsx | 52 +++++++++------- src/front/pages/TaskUser.jsx | 36 +++++------ src/front/styles/ModalOverride.css | 18 ++++++ 7 files changed, 225 insertions(+), 71 deletions(-) create mode 100644 src/front/components/TaskDetailModal.jsx create mode 100644 src/front/styles/ModalOverride.css diff --git a/src/app.py b/src/app.py index c33ab584f2..3671a8c978 100644 --- a/src/app.py +++ b/src/app.py @@ -18,7 +18,7 @@ app.url_map.strict_slashes = False CORS(app, resources={ r"/api/*": { - "origins": "https://organic-lamp-4446j5pq7jqhj7g4-3000.app.github.dev", + "origins": "https://shiny-xylophone-97xjjrww4vrp2766r-3000.app.github.dev", "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"], "supports_credentials": True @@ -42,16 +42,19 @@ app.register_blueprint(api_user, url_prefix='/api/users') app.register_blueprint(api_tasks, url_prefix='/api/users') + @app.errorhandler(APIException) def handle_invalid_usage(error): return jsonify(error.to_dict()), error.status_code + @app.route('/') def sitemap(): if ENV == "development": return generate_sitemap(app) return send_from_directory(static_file_dir, 'index.html') + @app.route('/', methods=['GET']) def serve_any_other_file(path): if not os.path.isfile(os.path.join(static_file_dir, path)): @@ -60,9 +63,7 @@ def serve_any_other_file(path): response.cache_control.max_age = 0 return response + 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/components/GoogleMaps.jsx b/src/front/components/GoogleMaps.jsx index ab64cd2c9a..f404749326 100644 --- a/src/front/components/GoogleMaps.jsx +++ b/src/front/components/GoogleMaps.jsx @@ -1,11 +1,49 @@ import React, { useState, useRef, useEffect } from "react"; -function GoogleMaps({ lat, lng, setLat, setLng }) { + +function GoogleMaps({ lat, lng, setLat, setLng, address, setAddress }) { const mapRef = useRef(null); const markerRef = useRef(null); + const [search, setSearch] = useState(""); + + const fetchAddress = (latitude, longitude) => { + const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; + fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${key}`) + .then(res => res.json()) + .then(data => { + if (data.status === "OK" && data.results.length > 0) { + // Filtrar para evitar códigos Plus y 'Sin Nombre' + const result = data.results.find(r => + !/^([A-Z0-9]{4,}\+\w{2,})/.test(r.formatted_address) && + !r.formatted_address.includes('Sin Nombre') + ); + setAddress(result ? result.formatted_address : ""); + } else { + setAddress(""); + } + }) + .catch(() => setAddress("")); + }; + + const fetchCoords = (direccion) => { + const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; + fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(direccion)}&key=${key}`) + .then(res => res.json()) + .then(data => { + if (data.status === "OK" && data.results.length > 0) { + const loc = data.results[0].geometry.location; + setLat(loc.lat); + setLng(loc.lng); + setAddress(data.results[0].formatted_address); + } + }); + }; useEffect(() => { + fetchAddress(lat, lng); + }, [lat, lng]); + useEffect(() => { function safeInitMap() { if (window.google && window.google.maps && typeof window.google.maps.Map === "function") { const center = { lat: lat ? parseFloat(lat) : 20, lng: lng ? parseFloat(lng) : -99 }; @@ -24,10 +62,12 @@ function GoogleMaps({ lat, lng, setLat, setLng }) { markerRef.current.position = e.latLng; setLat(e.latLng.lat()); setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); }); markerRef.current.addListener("dragend", (e) => { setLat(e.latLng.lat()); setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); }); } else { markerRef.current = new window.google.maps.Marker({ @@ -39,10 +79,12 @@ function GoogleMaps({ lat, lng, setLat, setLng }) { markerRef.current.setPosition(e.latLng); setLat(e.latLng.lat()); setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); }); markerRef.current.addListener("dragend", (e) => { setLat(e.latLng.lat()); setLng(e.latLng.lng()); + fetchAddress(e.latLng.lat(), e.latLng.lng()); }); } } else { @@ -50,7 +92,6 @@ function GoogleMaps({ lat, lng, setLat, setLng }) { } } - const key = import.meta.env.VITE_GOOGLE_MAPS_KEY || ''; const scriptId = "google-maps-script"; const existingScript = document.getElementById(scriptId); @@ -79,9 +120,24 @@ function GoogleMaps({ lat, lng, setLat, setLng }) { } else { safeInitMap(); } - - }, []); - return
; + }, [lat, lng]); + + return ( + <> + { + setAddress(e.target.value); + setSearch(e.target.value); + }} + onBlur={() => search && fetchCoords(search)} + style={{ width: "100%", marginBottom: 8, borderRadius: 8, padding: 8 }} + placeholder="Dirección" + /> +
+ + ); } export default GoogleMaps; diff --git a/src/front/components/ModalCreateTask.jsx b/src/front/components/ModalCreateTask.jsx index e06ea96a91..9ddf9f0575 100644 --- a/src/front/components/ModalCreateTask.jsx +++ b/src/front/components/ModalCreateTask.jsx @@ -15,6 +15,7 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { // Estados const [titulo, setTitulo] = useState(""); + const [fecha, setFecha] = useState(""); const [descripcion, setDescripcion] = useState(""); const [direccion, setDireccion] = useState(""); const [date, setDate] = useState(""); @@ -26,38 +27,48 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { useEffect(() => { if (taskToEdit) { setTitulo(taskToEdit.title || ""); + setFecha(taskToEdit.date || ""); setDescripcion(taskToEdit.description || ""); setDireccion(taskToEdit.address || ""); setLat(taskToEdit.latitude || 20); setLng(taskToEdit.longitude || -99); } else { - // Limpiar si es creación nueva setTitulo(""); + setFecha(""); setDescripcion(""); setDireccion(""); } }, [taskToEdit]); + // Solo un input de dirección, sincronizado con el mapa + + useEffect(() => { + if (taskToEdit) { + setTitulo(taskToEdit.title || ""); + setDescripcion(taskToEdit.description || ""); + setDireccion(taskToEdit.address || ""); + setLat(taskToEdit.latitude || 20); + setLng(taskToEdit.longitude || -99); + } else { + setTitulo(""); + setDescripcion(""); + setDireccion(""); + } + }, [taskToEdit]); + + // Sincronización: si el mapa cambia la dirección, actualiza el input + const handleMapAddressChange = (address) => { + setDireccion(address); + }; + const handleSubmit = (e) => { e.preventDefault(); setMsg(""); - - if (!titulo.trim()) { - setMsg("El título es obligatorio."); - return; - } - setTitulo(""); - setDescripcion(""); - setDireccion(""); - setDate(""); - setLat(""); - setLng(""); - setMsg("Tarea creada (mock)"); - const payloadData = { - id: taskToEdit ? taskToEdit.id : undefined, // Importante para editar + id: taskToEdit ? taskToEdit.id : undefined, title: titulo, + date: fecha, description: descripcion, address: direccion, latitude: lat, @@ -69,22 +80,26 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { setMsg("Error: No hay clan activo."); return; } - // Dispatch específico para CLAN (Crear o Editar) dispatch({ type: isEditing ? 'UPDATE_CLAN_TASK' : 'ADD_TASK_TO_CLAN', payload: { ...payloadData, clanId: activeClanId } }); } else { - // Dispatch específico para USER (Crear o Editar) dispatch({ type: isEditing ? 'UPDATE_USER_TASK' : 'ADD_USER_TASK', payload: payloadData }); } + setTitulo(""); + setDescripcion(""); + setDireccion(""); + setDate(""); + setLat(20); + setLng(-99); + setMsg(isEditing ? "Tarea actualizada" : "Tarea creada"); setShowTaskModal(false); }; - console.log(lat, lng) return (
@@ -92,19 +107,39 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) {
- {modalTitle}
- setTitulo(e.target.value)} style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} /> + setTitulo(e.target.value)} + style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} + /> + { + let v = e.target.value.replace(/[^0-9]/g, ""); + if (v.length > 2) v = v.slice(0, 2) + "/" + v.slice(2); + if (v.length > 5) v = v.slice(0, 5) + "/" + v.slice(5, 9); + if (v.length > 10) v = v.slice(0, 10); + setFecha(v); + }} + maxLength={10} + style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} + inputMode="numeric" + />
-
-
-
-
-
-
-
-
Redes
-
-
-
+
+
+
+ {showModal && ( +
+
+
+ +
+
Editar Perfil
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Redes
+
+
+
+
-
-
- - -
- +
+ + +
+ +
-
- )} + )} - {showBoteModal && ( -
-
-
-
-
-
Editar Saldo del Bote Personal
- -
-
-
- - setBoteAmount(e.target.value)} placeholder="0" /> + {showBoteModal && ( +
+
+
+ +
+
Editar Saldo del Bote Personal
+
-
-
- - -
- +
+
+ + setBoteAmount(e.target.value)} placeholder="0" /> +
+
+
+ + +
+ +
-
- )} + )} -
-
-
-
- Foto de perfil -

{store.profile.name}

-

{store.profile.email}

- -
+
+
+
+ + + - + -
-
Amigos activos
- {store.friends.map(friend => ())} +
+
Amigos activos
+ {store.friends.map(friend => ())} +
-
-
-
-
{tasksCompleted}
Tareas completadas
-
{tasksPending}
Tareas sin hacer
-
{clanCount}
Clan
-
+
+
+
{tasksCompleted}
Tareas completadas
+
{tasksPending}
Tareas sin hacer
+
{clanCount}
Clan
+
-
-
-
-

Detalles

-

{store.profile.bio}

-
-

{store.profile.city}

-

{store.profile.age} años

-

{store.profile.phone}

-

{store.profile.gender}

+
+
+
+

Detalles

+

{store.profile.bio}

+
+

{store.profile.city}

+

{store.profile.age} años

+

{store.profile.phone}

+

{store.profile.gender}

+
-
-
-
-

Mensajes

-

No hay mensajes nuevos.

+
+
+

Mensajes

+

No hay mensajes nuevos.

+
-
-
-
-

Otras redes

-

{store.profile.instagram || ""}

-

{store.profile.twitter || ""}

-

{store.profile.facebook || ""}

+
+
+

Otras redes

+

{store.profile.instagram || ""}

+

{store.profile.twitter || ""}

+

{store.profile.facebook || ""}

+
-
-
-
-

Gastos del Mes

- -

{totalExpenses.toFixed(2)}€

- Ver Detalles +
+
+

Gastos del Mes

+ +

{totalExpenses.toFixed(2)}€

+ Ver Detalles +
-
-
-
-
-

Saldo del Bote Personal

- +
+
+
+

Saldo del Bote Personal

+ +
+

{store.personalBote.toFixed(2)} €

-

{store.personalBote.toFixed(2)} €

diff --git a/src/front/styles/ProfileGroups.css b/src/front/styles/ProfileGroups.css index 27548e4401..427bd81345 100644 --- a/src/front/styles/ProfileGroups.css +++ b/src/front/styles/ProfileGroups.css @@ -273,19 +273,56 @@ body { text-decoration: none; } .user-profile-summary { - text-align: center; - margin-bottom: 30px; - padding: 15px; - background-color: rgba(0, 0, 0, 0.05); - border-radius: 10px; + display: flex; + align-items: center; + flex-direction: column; + text-align: center; + margin-bottom: 30px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 10px; } .user-avatar { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; - border: 3px solid #6366F1; - margin-bottom: 10px; + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #6366f1; + margin-bottom: 10px; + background: #6366f1; +} +.user-avatar img { + width: 100%; + height: 100%; +} +.user-profile-summary { + + display: flex; + align-items: center; + flex-direction: column; + text-align: center; + margin-bottom: 30px; + padding: 15px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 10px; +} +.user-avatar { +margin: 0 auto; + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #6366f1; + margin-bottom: 10px; + background: #6366f1; + color: white; +} +.user-avatar img { + width: 100%; + height: 100%; +} +.user-avatar span { + font-size: 3rem; } .username { display: block; @@ -335,7 +372,6 @@ body { flex-grow: 1; display: flex; flex-direction: column; - height: 100vh; overflow-y: auto; } @@ -655,4 +691,4 @@ calendar-month::part(range-inner) { calendar-month::part(button) { border-radius: 3px; -} \ No newline at end of file +} diff --git a/vite.config.js b/vite.config.js index a93495e81e..2eb5e0998c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,14 +1,12 @@ -import { - defineConfig -} from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; export default defineConfig({ - plugins: [react()], - server: { - port: 3000 - }, - build: { - outDir: 'dist' - } -}) \ No newline at end of file + plugins: [react()], + server: { + port: 3000, + }, + build: { + outDir: "dist", + }, +}); \ No newline at end of file From f822a77ba3df3d400cd60eeba0eccbcae17c500c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20Alfonso=20Mu=C3=B1oz=20=20Ttitua=C3=B1a?= Date: Wed, 26 Nov 2025 20:39:58 +0000 Subject: [PATCH 52/54] =?UTF-8?q?Agrego=20cambios=20para=20recuperacion=20?= =?UTF-8?q?de=20contrase=C3=B1a=20aun=20no=20funcional.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 1 + Pipfile.lock | 17 ++++++++--- index.html | 4 +-- src/api/extensions.py | 3 ++ src/api/routes.py | 23 +++++++++++--- src/app.py | 22 ++++++++++++- src/front/styles/navbar.css | 61 ------------------------------------- 7 files changed, 59 insertions(+), 72 deletions(-) create mode 100644 src/api/extensions.py delete mode 100644 src/front/styles/navbar.css diff --git a/Pipfile b/Pipfile index 9eebabf107..5903f3966a 100644 --- a/Pipfile +++ b/Pipfile @@ -20,6 +20,7 @@ typing-extensions = "*" wtforms = "==3.1.2" sqlalchemy = "*" flask-jwt-extended = "*" +flask-mail = "*" [requires] python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index 2b8991ed42..fa578d2936 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1e3cc14c317d2e1d7cccecd1fcc7afd5b60c23767c0d3997b1db5574a144d218" + "sha256": "b442b84d151e30e7f38c6a763287027ca60d24914f9af5b0cbea537b813d560b" }, "pipfile-spec": 6, "requires": { @@ -42,11 +42,11 @@ }, "click": { "hashes": [ - "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", - "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], "markers": "python_version >= '3.10'", - "version": "==8.3.0" + "version": "==8.3.1" }, "cloudinary": { "hashes": [ @@ -90,6 +90,15 @@ "markers": "python_version >= '3.9' and python_version < '4'", "version": "==4.7.1" }, + "flask-mail": { + "hashes": [ + "sha256:44083e7b02bbcce792209c06252f8569dd5a325a7aaa76afe7330422bd97881d", + "sha256:a451e490931bb3441d9b11ebab6812a16bfa81855792ae1bf9c1e1e22c4e51e7" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.10.0" + }, "flask-migrate": { "hashes": [ "sha256:1a336b06eb2c3ace005f5f2ded8641d534c18798d64061f6ff11f79e1434126d", diff --git a/index.html b/index.html index 27a99f796e..644cfd5cb9 100644 --- a/index.html +++ b/index.html @@ -2,11 +2,11 @@ - + - Hello Rigo + TaskFlow
diff --git a/src/api/extensions.py b/src/api/extensions.py new file mode 100644 index 0000000000..3d1d121617 --- /dev/null +++ b/src/api/extensions.py @@ -0,0 +1,3 @@ +from flask_mail import Mail + +mail = Mail() \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index de7e531b72..a2e613221e 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -2,10 +2,14 @@ import secrets from werkzeug.security import generate_password_hash from api.models import db, User - +import os +from flask_mail import Message +from api.extensions import mail api = Blueprint('api', __name__) reset_tokens = {} +url_front = os.getenv("VITE_FRONTEND") + @api.route('/forgot-password', methods=['POST']) def forgot_password(): @@ -16,9 +20,20 @@ def forgot_password(): user = User.query.filter_by(email=email).first() if not user: return jsonify({"msg": "Usuario no encontrado o no registrado"}), 404 - token = secrets.token_urlsafe(16) - reset_tokens[email] = token - return jsonify({"msg": "Token de recuperación generado", "token": token}), 200 + + reset_email = f"{url_front}/resetPassword/token" + + msg = Message( + 'Recupera contraseña', + html=f"

Da click Aqui para recuperar tu contraseña.

", + recipients=[email], + sender='taskflowproyect@gmail.com' + ) + + mail.send(msg) + + return jsonify({"msg": "Correo enviado exitosamente"}), 200 + @api.route('/reset-password', methods=['POST']) def reset_password(): diff --git a/src/app.py b/src/app.py index 1e04a0c504..c4ce89df95 100644 --- a/src/app.py +++ b/src/app.py @@ -10,11 +10,16 @@ from api.admin import setup_admin from api.commands import setup_commands from api.routesTasks import api_tasks +from api.extensions import mail +from flask_mail import Mail + 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__) + + app.url_map.strict_slashes = False CORS(app, resources={ r"/api/*": { @@ -32,9 +37,24 @@ app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:////tmp/test.db" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +app.config.update(dict( + DEBUG=False, + MAIL_SERVER='smtp.gmail.com', + MAIL_PORT=587, + MAIL_USE_TLS=True, + MAIL_USE_SSL=False, + MAIL_USERNAME='taskflowproyect@gmail.com', + MAIL_PASSWORD=os.getenv('MAIL_PASSWORD') +)), + + MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) + +mail.init_app(app) + setup_admin(app) setup_commands(app) @@ -66,4 +86,4 @@ def serve_any_other_file(path): if __name__ == '__main__': PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + app.run(host='0.0.0.0', port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/styles/navbar.css b/src/front/styles/navbar.css deleted file mode 100644 index fad8c24094..0000000000 --- a/src/front/styles/navbar.css +++ /dev/null @@ -1,61 +0,0 @@ - -/* Ajustes del Logo*/ -.logo { - width: 50px; - height: auto; -} -/* Navegación Superior (Navbar) */ -.dashboard-navbar { - background-color: rgba(31, 42, 64, 0.6); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - padding: 10px 20px; - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - position: sticky; - top: 0; - z-index: 999; -} -.navbar-left { - display: flex; - align-items: center; - gap: 20px; -} -.navbar-right { - display: flex; - align-items: center; - gap: 25px; -} -.search-bar { - display: flex; - align-items: center; - background-color: rgba(255, 255, 255, 0.1); - border-radius: 20px; - padding: 8px 15px; -} -.search-bar i { - color: #9ab4d1; - margin-right: 10px; -} -.search-bar input { - background: transparent; - border: none; - color: #e0e6ed; - width: 250px; - outline: none; -} -.search-bar input::placeholder { - color: #9ab4d1; -} -.navbar-icons i { - font-size: 1.2rem; - color: #9ab4d1; - margin-left: 20px; - cursor: pointer; - transition: color 0.3s ease; -} -.navbar-icons i:hover { - color: #1e91ed; -} From f02c7a23bbd9e6a711bf3b763d0c60686e1ca5ee Mon Sep 17 00:00:00 2001 From: JorgeCKR Date: Thu, 27 Nov 2025 13:00:53 +0000 Subject: [PATCH 53/54] Tareas conectadas con Back --- src/api/routesTasks.py | 4 +- src/front/components/ModalCreateTask.jsx | 128 ++++++++++++++++------- src/front/pages/Dashboard.jsx | 78 +++++++++++++- src/front/store.js | 6 +- 4 files changed, 172 insertions(+), 44 deletions(-) diff --git a/src/api/routesTasks.py b/src/api/routesTasks.py index e09684337f..8da44fa139 100644 --- a/src/api/routesTasks.py +++ b/src/api/routesTasks.py @@ -70,7 +70,6 @@ def agregar_tarea_user(user_id): title = data.get("title") description = data.get("description") - date = data.get("date") lat = data.get("lat") lng = data.get("lng") estado_id = data.get("estado_id") @@ -78,12 +77,11 @@ def agregar_tarea_user(user_id): prioridad_id = data.get("prioridad_id") if not title: - return jsonify({"msg": "El título es obligatorio"}), 400 + return jsonify({"msg": "El title es obligatorio"}), 400 nueva_tarea = Task( title=title, description=description, - date=date, lat=lat, lng=lng, estado_id=estado_id, diff --git a/src/front/components/ModalCreateTask.jsx b/src/front/components/ModalCreateTask.jsx index 771fa51c9d..7fd6711b5c 100644 --- a/src/front/components/ModalCreateTask.jsx +++ b/src/front/components/ModalCreateTask.jsx @@ -19,8 +19,8 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { const [descripcion, setDescripcion] = useState(""); const [direccion, setDireccion] = useState(""); const [date, setDate] = useState(""); - const [lat, setLat] = useState(20); - const [lng, setLng] = useState(-99); + const [lat, setLat] = useState(""); + const [lng, setLng] = useState(""); const [msg, setMsg] = useState(""); @@ -30,8 +30,8 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { setFecha(taskToEdit.date || ""); setDescripcion(taskToEdit.description || ""); setDireccion(taskToEdit.address || ""); - setLat(taskToEdit.latitude || 20); - setLng(taskToEdit.longitude || -99); + setLat(taskToEdit.lat || ""); + setLng(taskToEdit.lng || ""); } else { setTitulo(""); setFecha(""); @@ -47,8 +47,8 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { setTitulo(taskToEdit.title || ""); setDescripcion(taskToEdit.description || ""); setDireccion(taskToEdit.address || ""); - setLat(taskToEdit.latitude || 20); - setLng(taskToEdit.longitude || -99); + setLat(taskToEdit.lat || ""); + setLng(taskToEdit.lng || ""); } else { setTitulo(""); setDescripcion(""); @@ -61,44 +61,100 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { setDireccion(address); }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); setMsg(""); + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const payloadData = { - id: taskToEdit ? taskToEdit.id : undefined, title: titulo, - date: fecha, description: descripcion, - address: direccion, - latitude: lat, - longitude: lng, + lat: lat, + lng: lng, + estado_id: null, + evento_id: null, + prioridad_id: null }; - if (taskType === 'clan') { - if (!isEditing && !activeClanId) { - setMsg("Error: No hay clan activo."); - return; + try { + if (taskType === "user") { + + // CREAR + if (!isEditing) { + const response = await fetch( + `${backendUrl}/api/users/${store.user.id}/tareas`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${store.token}` + }, + body: JSON.stringify(payloadData) + } + ); + + // --- DEBUG --- + console.log("STATUS:", response.status); + console.log("HEADERS:", response.headers); + + // usamos text() en lugar de json() para ver TODO lo que responde Flask + const text = await response.text(); + console.log("RAW BACKEND RESPONSE:", text); + + let data; + try { + data = JSON.parse(text); + } catch (err) { + console.error("La respuesta NO es JSON válido:", text); + } + // --- FIN DEBUG --- + + if (!response.ok) { + setMsg(data.msg || "Error creando tarea"); + return; + } + + dispatch({ + type: "ADD_USER_TASK", + payload: data.tarea + }); + + } + // EDITAR + else { + const response = await fetch( + `${backendUrl}/api/users/${store.user.id}/tareas/${taskToEdit.id}/editar`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${store.token}` + }, + body: JSON.stringify(payloadData) + } + ); + + const data = await response.json(); + + if (!response.ok) { + setMsg(data.msg || "Error editando tarea"); + return; + } + + dispatch({ + type: "UPDATE_USER_TASK", + payload: data.Tarea + }); + } } - dispatch({ - type: isEditing ? 'UPDATE_CLAN_TASK' : 'ADD_TASK_TO_CLAN', - payload: { ...payloadData, clanId: activeClanId } - }); - } else { - dispatch({ - type: isEditing ? 'UPDATE_USER_TASK' : 'ADD_USER_TASK', - payload: payloadData - }); - } - setTitulo(""); - setDescripcion(""); - setDireccion(""); - setDate(""); - setLat(20); - setLng(-99); - setMsg(isEditing ? "Tarea actualizada" : "Tarea creada"); - setShowTaskModal(false); + setShowTaskModal(false); + + } catch (error) { + console.error("Error conectando con backend:", error); + setMsg("No se pudo conectar con el servidor"); + } }; return ( @@ -117,7 +173,7 @@ function ModalCreateTask({ setShowTaskModal, taskType, taskToEdit = null }) { onChange={e => setTitulo(e.target.value)} style={{ width: "100%", marginBottom: 12, border: "1px solid #1e91ed", borderRadius: 8, padding: 10 }} /> - +