diff --git a/backend/api/models/docit2mf_models.py b/backend/api/models/docit2mf_models.py index d8069cb..8464a9a 100644 --- a/backend/api/models/docit2mf_models.py +++ b/backend/api/models/docit2mf_models.py @@ -1,6 +1,6 @@ # models/docit2mf_models.py -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, Field, field_validator, ValidationInfo from typing import List, Tuple, Union @@ -9,8 +9,8 @@ BlankCardInput = Union[int, Tuple[int, int], List[int]] class DoCIT2MFRequest(BaseModel): term: str - core: Tuple[float, float] - support: Tuple[float, float] + core: tuple[float, float] = Field(..., description="Núcleo del conjunto difuso: [a, b]") + support: tuple[float, float] = Field(..., description="Soporte del conjunto difuso: [c, d]") left_nodes_x: List[float] left_blank_cards: List[BlankCardInput] @@ -28,20 +28,20 @@ class DoCIT2MFRequest(BaseModel): def core_valid(cls, v): a, b = v if a > b: - raise ValueError("El núcleo debe cumplir a <= b.") + raise ValueError("el 'Inicio del Núcleo' debe ser menor o igual al 'Fin del Núcleo'.") return v @field_validator("support") - def support_valid(cls, v, info): + def support_valid(cls, v, info: ValidationInfo): c, d = v - if c >= d: - raise ValueError("El soporte debe cumplir c < d.") + if c > d: + raise ValueError("el 'Inicio del Soporte' debe ser menor o igual al 'Fin del Soporte'.") core = info.data.get("core") if core: a, b = core - if not (c <= a <= b <= d): - raise ValueError("El núcleo debe estar dentro del soporte.") + if not (c <= a and b <= d): + raise ValueError("los valores del 'Núcleo' deben estar estrictamente dentro del 'Soporte'.") return v @field_validator("left_blank_cards", "right_blank_cards") diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py index ba4c06a..f0761aa 100644 --- a/backend/api/routers/docit2mf_build.py +++ b/backend/api/routers/docit2mf_build.py @@ -1,7 +1,7 @@ # api/routers/docit2mf_build.py import logging -from fastapi import APIRouter, HTTPException +from fastapi import APIRouter, HTTPException, Request from slowapi import Limiter from slowapi.util import get_remote_address from api.models.docit2mf_models import DoCIT2MFMultiRequest @@ -14,11 +14,11 @@ logger = logging.getLogger(__name__) @router.post("/doc-it2mf/build") @limiter.limit("10/minute") -async def build_doc_it2mf(request: DoCIT2MFMultiRequest): +async def build_doc_it2mf(request: Request, body: DoCIT2MFMultiRequest): results = [] try: - for level in request.levels: + for level in body.levels: results.append(build_it2mf_from_level(level)) except ValueError as e: logger.warning(f"Validation error in doc-it2mf/build: {str(e)}") @@ -27,4 +27,4 @@ async def build_doc_it2mf(request: DoCIT2MFMultiRequest): logger.error(f"Unexpected error in doc-it2mf/build: {str(e)}", exc_info=True) raise HTTPException(status_code=500, detail="Internal server error") - return {"levels": results} + return {"levels": results} \ No newline at end of file diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py index c7e5f77..96734a2 100644 --- a/backend/api/routers/google_auth.py +++ b/backend/api/routers/google_auth.py @@ -1,19 +1,18 @@ -# api/routers/google_auth.py - from fastapi import APIRouter, HTTPException, Request from fastapi.responses import RedirectResponse +from datetime import datetime, timedelta import httpx import os import jwt from api.database.mongodb import users_collection -from api.utils.security import create_access_token router = APIRouter(prefix="/auth/google", tags=["auth"]) GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") +SECRET_KEY = os.getenv("SECRET_KEY") # ----------------------------- @@ -90,6 +89,20 @@ async def google_callback(request: Request): else: user_id = user["_id"] - token = create_access_token({"user_id": str(user_id)}) + token = jwt.encode( + { + "sub": str(user_id), + "email": email, + "name": name, + "exp": datetime.utcnow() + timedelta(hours=24) + }, + SECRET_KEY, + algorithm="HS256" + ) - return RedirectResponse(f"http://localhost:5173/login?token={token}") + await users_collection.update_one( + {"_id": user_id}, + {"$set": {"token": token}} + ) + + return RedirectResponse(f"http://localhost:5173/login?token={token}") \ No newline at end of file diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py index 82083ac..77ca7cb 100644 --- a/backend/api/services/docit2mf_build_service.py +++ b/backend/api/services/docit2mf_build_service.py @@ -24,7 +24,7 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li def _sort_nodes(nodes): """Ordena los nodos por su coordenada X.""" - return sorted(nodes, key=lambda p: p[0]) + return sorted([list(p) for p in nodes], key=lambda p: p[0]) def _enforce_upper_ge_lower(lower, upper): @@ -34,14 +34,14 @@ def _enforce_upper_ge_lower(lower, upper): """ # left nodes for i in range(len(lower["left_nodes"])): - lx, ly = lower["left_nodes"][i] - ux, uy = upper["left_nodes"][i] + ly = lower["left_nodes"][i][1] + uy = upper["left_nodes"][i][1] upper["left_nodes"][i][1] = max(uy, ly) # right nodes for i in range(len(lower["right_nodes"])): - lx, ly = lower["right_nodes"][i] - ux, uy = upper["right_nodes"][i] + ly = lower["right_nodes"][i][1] + uy = upper["right_nodes"][i][1] upper["right_nodes"][i][1] = max(uy, ly) return upper @@ -73,9 +73,13 @@ def build_it2mf_from_level(level: DoCIT2MFRequest): right_nodes_x=level.right_nodes_x, right_blank_cards=right_min, ) - lower = build_doc_mf_level(lower_level) - # Ordenar nodos LMF + # Convertir a dict para poder manipular como diccionario + lower = build_doc_mf_level(lower_level) + if hasattr(lower, "model_dump"): + lower = lower.model_dump() + + # Asegurar que los nodos son listas mutables y ordenar lower["left_nodes"] = _sort_nodes(lower["left_nodes"]) lower["right_nodes"] = _sort_nodes(lower["right_nodes"]) @@ -94,9 +98,13 @@ def build_it2mf_from_level(level: DoCIT2MFRequest): right_nodes_x=level.right_nodes_x, right_blank_cards=right_max, ) - upper = build_doc_mf_level(upper_level) - # Ordenar nodos UMF + # Convertir a dict para poder manipular como diccionario + upper = build_doc_mf_level(upper_level) + if hasattr(upper, "model_dump"): + upper = upper.model_dump() + + # Asegurar que los nodos son listas mutables y ordenar upper["left_nodes"] = _sort_nodes(upper["left_nodes"]) upper["right_nodes"] = _sort_nodes(upper["right_nodes"]) @@ -109,4 +117,4 @@ def build_it2mf_from_level(level: DoCIT2MFRequest): "term": level.term, "lower": lower, "upper": upper - } + } \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c0cf46f..6727ebe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" @@ -3077,6 +3078,15 @@ "react": "^19.2.4" } }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index debc10c..6b49322 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..cd55914 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file +logo doc \ No newline at end of file diff --git a/frontend/public/uja-logo.png b/frontend/public/uja-logo.png new file mode 100644 index 0000000..77d17fd Binary files /dev/null and b/frontend/public/uja-logo.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index df64ca3..e05ea1a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,4 @@ -import { AppRouter } from './routers/AppRouter'; +import AppRouter from './routers/AppRouter'; function App() { return ( diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index b35b7ed..3292eea 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -1,3 +1,5 @@ +import React from 'react'; + export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { const maxCardsPerRow = 7; @@ -7,35 +9,29 @@ export default function BlankCardsCounter({ index, blankCardsCount, handleBlankC } return ( -
+
-
+ {/* Bloque de botones */} +
+ - {/* Línea conectora horizontal */} -
- - {/* Botones - y + */} -
- - -
- Blancas - {blankCardsCount} -
- - +
+ Blancas + {blankCardsCount}
+ +
{/* Cartas blancas */} {blankCardsCount > 0 && ( -
+
{rows.map((row, rowIndex) => (
{row.map((_, colIndex) => ( diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index c5e887f..4783869 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -10,9 +10,9 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemo )} {index + 1} {index + 1} - handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} /> + handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} />
-
{error &&

Escribe una etiqueta

}
+
{error &&

Escribe un término

}
); } \ No newline at end of file diff --git a/frontend/src/components/EyeIcon.jsx b/frontend/src/components/EyeIcon.jsx new file mode 100644 index 0000000..70437ac --- /dev/null +++ b/frontend/src/components/EyeIcon.jsx @@ -0,0 +1,12 @@ +export default function EyeIcon({ isOpen }) { + return isOpen ? ( + + + + + ) : ( + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/Step1BaseScale.jsx b/frontend/src/components/editor/Step1BaseScale.jsx index 115b450..7bdb24b 100644 --- a/frontend/src/components/editor/Step1BaseScale.jsx +++ b/frontend/src/components/editor/Step1BaseScale.jsx @@ -37,7 +37,7 @@ export default function Step1BaseScale({ const currentScale = isZoomActive && needsZoom ? dynamicScale : 1; return ( -
+

@@ -59,30 +59,48 @@ export default function Step1BaseScale({ -
+
{levels.map((level, index) => ( -
+ + {/* CARTA DE NIVEL */} +
3} />
+ + {/* HUECO ENTRE CARTAS Y CONTADOR */} {index < levels.length - 1 && ( - +
+
+ +
+ +
+
)} ))} -
-
+ + {/* LÍNEA HACIA EL BOTÓN DE AÑADIR */} +
+
- + + {/* BOTÓN AÑADIR NIVEL */} +
+ +
+
-
+ {/* Generar Gráfica Continua */} +
diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index 34ba6f8..2369c54 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -11,7 +11,8 @@ export default function Step2FuzzyModeling({ handleFinalSubmit, onBack, subscales, - onOpenSubscale + onOpenSubscale, + submitError }) { const scaleKeys = Object.keys(baseScale); @@ -51,6 +52,20 @@ export default function Step2FuzzyModeling({ colors={CHART_COLORS} /> + {submitError && ( +
+
+ ⚠️ +
+

Error de validación al generar la gráfica

+
+ {submitError} +
+
+
+
+ )} +
diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 45c9d27..8e344f9 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -1,103 +1,92 @@ -import { useMemo } from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -import { CHART_COLORS } from '../../config'; +import React, { useState, useEffect, memo } from 'react'; +import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { useGraphData } from './finalGraph/useGraphData'; +import { GraphTooltip } from './finalGraph/GraphTooltip'; -const Step3FinalGraph = ({ data }) => { - const sortedResults = useMemo(() => { - if (!data || !data.results) return []; +const Step3FinalGraph = memo(({ data, criterionName }) => { + const { sortedResults, denseData } = useGraphData(data); + const [isReady, setIsReady] = useState(false); - const withPermanentColors = data.results.map((item, index) => ({ - ...item, - color: CHART_COLORS[index % CHART_COLORS.length] - })); + useEffect(() => { + const timer = setTimeout(() => { + setIsReady(true); + }, 400); + return () => clearTimeout(timer); + }, []); - return withPermanentColors.sort((a, b) => { - const coreA = Array.isArray(a.core) ? Number(a.core[0]) : 0; - const coreB = Array.isArray(b.core) ? Number(b.core[0]) : 0; - return coreA - coreB; - }); - }, [data]); - - if (!data || !data.results) { - return

Cargando gráfico final...

; + if (!data || (!data.levels && !data.results)) { + return

Cargando datos...

; } return ( -
-

Espectro Difuso Final

+
+ - {/* Gráfica */} -
- - - - - - [Number(value).toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} - /> +

+ {criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'} +

+ +
+ + {!isReady && ( +
+
+ Generando gráfica... +
+ )} - {sortedResults.map((item) => { - const lineData = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(node => ({ - x: Number(node[0]), - y: Number(node[1]) - })); - - return ( - + {isReady && ( + + + + - ); - })} - - + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} + /> + + } + cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }} + isAnimationActive={false} + /> + + {sortedResults.map((item) => ( + + {item.isType2 ? ( + <> + + + + + ) : ( + + )} + + ))} + + + )} +
{/* Leyenda */} -
+
{sortedResults.map((item) => ( -
- - - - - {item.term} - -
+
+ + {item.term} +
))}
-
); -}; +}); export default Step3FinalGraph; \ No newline at end of file diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 70bb4ce..6f70c3a 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -3,30 +3,78 @@ import BlankCardsCounter from '../BlankCardsCounter'; export default function SubscaleModal({ onClose, onSave, targetInfo }) { - const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); - const [blankCards, setBlankCards] = useState(targetInfo?.initialData?.blankCards || [0]); + const initialCount = Math.max(3, targetInfo?.initialData?.cardsCount || 3); + const [cardsCount, setCardsCount] = useState(initialCount); + + const [blankCards, setBlankCards] = useState(() => { + let initialBlanks = targetInfo?.initialData?.blankCards; + + if (!initialBlanks || initialBlanks.length === 0) { + initialBlanks = [0, 0]; + } else if (initialBlanks.length < initialCount - 1) { + const padding = Array(initialCount - 1 - initialBlanks.length).fill(0); + initialBlanks = [...initialBlanks, ...padding]; + } + + return initialBlanks.map(b => { + if (Array.isArray(b)) { + return { min: b[0], max: b[1], isRange: true }; + } + return { min: b, max: b, isRange: false }; + }); + }); const handleAddCard = () => { setCardsCount(prev => prev + 1); - setBlankCards([...blankCards, 0]); + setBlankCards([...blankCards, { min: 0, max: 0, isRange: false }]); }; const handleRemoveCard = () => { - if (cardsCount <= 2) return; + if (cardsCount <= 3) return; setCardsCount(prev => prev - 1); setBlankCards(blankCards.slice(0, -1)); }; - const handleBlankCardChange = (index, delta) => { + const handleExactChange = (index, delta) => { const newBlanks = [...blankCards]; - if (newBlanks[index] + delta >= 0) { - newBlanks[index] += delta; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0) { + newBlanks[index].min = newVal; + newBlanks[index].max = newVal; setBlankCards(newBlanks); } }; + const handleMinChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0 && newVal <= newBlanks[index].max) { + newBlanks[index].min = newVal; + setBlankCards(newBlanks); + } + }; + + const handleMaxChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].max + delta; + if (newVal >= newBlanks[index].min) { + newBlanks[index].max = newVal; + setBlankCards(newBlanks); + } + }; + + const toggleRangeMode = (index) => { + const newBlanks = [...blankCards]; + newBlanks[index].isRange = !newBlanks[index].isRange; + if (!newBlanks[index].isRange) { + newBlanks[index].max = newBlanks[index].min; + } + setBlankCards(newBlanks); + }; + const handleSave = () => { - onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards }); + const payloadBlanks = blankCards.map(b => b.isRange ? [b.min, b.max] : b.min); + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards: payloadBlanks }); }; const handleDelete = () => { @@ -34,10 +82,10 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { }; return ( -
-
+
+
-
+

Diseñar Subescala

@@ -47,35 +95,84 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {

- {/* Tablero */} -
+
+ {Array.from({ length: cardsCount }).map((_, index) => ( -
+ {/* CARTA DE REFERENCIA */} +
- {cardsCount > 2 && index === cardsCount - 1 && ( - + {cardsCount > 3 && index === cardsCount - 1 && ( + )} {index + 1}
+ + {/* HUECO ENTRE CARTAS */} {index < cardsCount - 1 && ( - +
+
+ +
+ {blankCards[index].isRange ? ( +
+
+ MÍN + handleMinChange(idx, delta)} + /> +
+ +
-
+ +
+ MÁX + handleMaxChange(idx, delta)} + /> +
+
+ ) : ( +
+ CARTAS + handleExactChange(idx, delta)} + /> +
+ )} + + +
+
)} ))} -
-
+
- {/* Botones */} -
+ {/* Botones de Acción */} +
diff --git a/frontend/src/components/editor/finalGraph/GraphTooltip.jsx b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx new file mode 100644 index 0000000..bc95ac9 --- /dev/null +++ b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx @@ -0,0 +1,53 @@ +const TermInfo = ({ title, color, children }) => ( +
+ {title} + {children} +
+); + +export const GraphTooltip = ({ active, payload, label, sortedResults }) => { + if (!active || !payload || !payload.length) return null; + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => + item.isType2 ? (dataPoint[`${item.term}_upper`] ?? 0) > 0 : (dataPoint[item.term] ?? 0) > 0 + ); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] ?? 0; + const upper = dataPoint[`${item.term}_upper`] ?? 0; + const range = Math.abs(upper - lower); + + return range <= 0.001 ? ( + + Pertenencia: {Number(upper).toFixed(3)} + + ) : ( + + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + + + ); + } + return ( + + Pertenencia: {Number(dataPoint[item.term]).toFixed(3)} + + ); + })} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js new file mode 100644 index 0000000..408cdfc --- /dev/null +++ b/frontend/src/components/editor/finalGraph/useGraphData.js @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import { CHART_COLORS } from '../../../config'; + +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) return nodes[i][1]; + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + if (Math.abs(x2 - x1) < EPSILON) continue; + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + +export const useGraphData = (data) => { + const sortedResults = useMemo(() => { + const rawItems = data?.levels || data?.results || []; + const processed = rawItems.map((item, index) => { + const isType2 = !!item.lower && !!item.upper; + const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; + let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; + + if (isType2) { + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; + } else { + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; + } + }); + return processed.sort((a, b) => a.coreVal - b.coreVal); + }, [data]); + + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + for (let i = 0; i <= steps; i++) xSet.add(Number((i / steps).toFixed(4))); + + sortedResults.forEach(item => { + const addNodes = (nodes) => nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + item.isType2 ? (addNodes(item.lowerNodes), addNodes(item.upperNodes)) : addNodes(item.nodes); + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + return xValues.map(x => { + const point = { x }; + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + point[`${item.term}_range`] = (lowerRaw === null && upperRaw === null) ? null : [lowerRaw ?? 0, upperRaw ?? 0]; + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + return point; + }); + }, [sortedResults]); + + return { sortedResults, denseData }; +}; \ No newline at end of file diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx new file mode 100644 index 0000000..4bb3a2f --- /dev/null +++ b/frontend/src/components/layout/Footer.jsx @@ -0,0 +1,95 @@ +export default function Footer() { + return ( +
+
+ +
+ + {/* Proyecto */} +
+
+ Deck of Cards + + Software Científico + +
+

+ Plataforma web para la elicitación de escalas de valor y construcción de conjuntos difusos interpretables (DoC-MF). +

+
+ + {/* Desarrollo */} +
+

Ingeniería y Desarrollo

+
    +
  • + Alexis López Moral + Frontend +
  • +
  • + Mireya Cueto Garrido + Backend +
  • +
+
+ + {/* Dirección Científica */} +
+

Dirección Científica

+

Luis Martínez López

+
+ + {/* Enlaces Institucionales y Código */} +
+ + {/* Universidad de Jaén */} + +
+ Universidad + de Jaén +
+ Logo UJA +
+ + {/* Repositorio GitHub */} + +
+ Repositorio + Oficial +
+ +
+ +
+
+ + {/* Sub-Footer: Copyright y Referencia Científica */} +
+

+ © {new Date().getFullYear()} Deck of Cards App. +

+

+ Basado en la metodología DoC-MF propuesta por D. García-Zamora, B. Dutta, J.R. Figueira y L. Martínez (EJOR, 2024). +

+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx new file mode 100644 index 0000000..2011ca7 --- /dev/null +++ b/frontend/src/components/layout/Header.jsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import { FiLogIn, FiLogOut } from 'react-icons/fi'; + +export default function Header() { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + const { user, logout, isAuthenticated } = useAuth(); + + const userInitial = user?.username ? user.username[0].toUpperCase() : "U"; + + const handleLogout = () => { + logout(); + setIsDropdownOpen(false); + navigate('/login'); + }; + + const isActive = (path) => { + return location.pathname === path || (path === '/editor' && location.pathname === '/'); + }; + + return ( +
+
+ + + Deck of Cards Logo + + Deck of Cards + + + +
+
+ + Editor + + {isAuthenticated && ( + + Historial + + )} +
+ + {isAuthenticated ? ( +
+ + + {isDropdownOpen && ( + <> +
setIsDropdownOpen(false)}>
+
+
+

Usuario

+

{user?.username}

+
+ + +
+ + )} +
+ ) : ( +
+ + + + Acceder + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index c8bc184..b6d6da7 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -1,26 +1,18 @@ -import { Outlet } from 'react-router-dom'; +import Header from './Header'; +import Footer from './Footer'; -export default function MainLayout() { +export default function MainLayout({ children }) { return ( -
+
- {/* Cabecera */} -
-
-
- DoC -
-

- Deck of Cards -

-
-
+
- {/* Contenido principal */} -
- +
+ {children}
+
+
); } \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index f879c4a..565cc84 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -22,7 +22,6 @@ export default function Controls({

- {/* Lado izquierdo (Pendiente ascendente) */}
- {/* Botón subescala izquierda */}
- {/* Lado derecho (Pendiente descendente) */}
-
- - updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> -
updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
- {/* Botón subescala derecha */}
- )} -
- - - -
-
- -
- - {levels.map((level, index) => ( - -
- 3} /> -
- {index < levels.length - 1 && ( - - )} -
- ))} - -
-
-
- - -
- -
-
- -
- -
-
- )} - - {/* PASO 2 */} - {step === 2 && ( -
-
-

Paso 2: Modelar Conceptos Difusos

- -
- -
- {scaleKeys.map((name, index) => { - const color = COLORS[index % COLORS.length]; - const isSelected = selectedTerm === name; - return ( - - ); - })} -
- - - - - -
- -
-
- )} -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx deleted file mode 100644 index 3d913e6..0000000 --- a/frontend/src/pages/BasicMode.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useState } from 'react'; -import CriterionInput from '../components/CriterionInput'; -import CardEditor from '../components/CardEditor'; -import BlankCardsCounter from '../components/BlankCardsCounter'; -import AddLevelButton from '../components/AddLevelButton'; -import ValueFunctionChart from '../components/ValueFunctionChart'; -import { calculateValueFunction } from '../services/docService'; - -export default function BasicMode() { - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - - const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); - - const [errors, setErrors] = useState({ criterion: false, levels: [] }); - - const handleCalculate = async () => { - - let hasError = false; - const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; - - if (!criterionName.trim()) { - newErrors.criterion = true; - hasError = true; - } - - levels.forEach((level, idx) => { - if (!level.trim()) { - newErrors.levels[idx] = true; - hasError = true; - } - }); - - setErrors(newErrors); - - if (hasError) return; - - setIsLoading(true); - setResult(null); - - const payload = { - criterion_name: criterionName.trim(), - levels: levels.map(l => l.trim()), - blank_cards: blankCards, - references: { "0": 0, [(levels.length - 1).toString()]: 1 } - }; - - try { - const data = await calculateValueFunction(payload); - setResult(data); - } catch (error) { - alert("No se ha podido conectar con el backend: " + error); - } finally { - setIsLoading(false); - } - }; - - const handleCriterionChange = (val) => { - setCriterionName(val); - if (errors.criterion) setErrors({ ...errors, criterion: false }); - }; - - const handleLevelChange = (index, newValue) => { - const newLevels = [...levels]; - newLevels[index] = newValue; - setLevels(newLevels); - - if (errors.levels[index]) { - const newErrLevels = [...errors.levels]; - newErrLevels[index] = false; - setErrors({ ...errors, levels: newErrLevels }); - } - }; - - const handleAddLevel = () => { - setLevels([...levels, '']); - setBlankCards([...blankCards, 0]); - setErrors({ ...errors, levels: [...errors.levels, false] }); - }; - - const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 3) return; - const newLevels = levels.filter((_, index) => index !== indexToRemove); - const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; - const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); - - const newErrLevels = errors.levels.filter((_, index) => index !== indexToRemove); - - setLevels(newLevels); - setBlankCards(newBlankCards); - setErrors({ ...errors, levels: newErrLevels }); - }; - - const handleBlankCardChange = (index, delta) => { - const newBlankCards = [...blankCards]; - const newValue = newBlankCards[index] + delta; - if (newValue >= 0) { - newBlankCards[index] = newValue; - setBlankCards(newBlankCards); - } - }; - - return ( -
- - - -
- {levels.map((level, index) => ( -
- - - - {index < levels.length - 1 && ( - - )} -
- ))} - - -
- -
- -
- - - -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 4cdc508..8e318ab 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; -import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; +import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService'; import Step3FinalGraph from '../components/editor/Step3FinalGraph'; export default function DocEditor() { @@ -24,6 +24,7 @@ export default function DocEditor() { // ESTADO: FASE 3 const [finalResult, setFinalResult] = useState(null); + const [submitError, setSubmitError] = useState(null); // MANEJADORES: FASE 1 const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; @@ -119,6 +120,7 @@ export default function DocEditor() { // Petición para el endpoint "build" const handleFinalSubmit = async () => { + setSubmitError(null); const scaleKeys = Object.keys(baseScale); const payload = { @@ -157,14 +159,64 @@ export default function DocEditor() { setIsLoading(true); try { const result = await buildFuzzyGraph(payload); - console.log("RESPUESTA DEL BACKEND:", result); - setFinalResult(result); setStep(3); - } catch (error) { - console.error(error); - alert("Error del servidor: \n" + JSON.stringify(error, null, 2)); + + let friendlyMessage = "Ocurrió un error al procesar la solicitud."; + const errorData = error.backendData || error.response?.data || error; + + if (errorData.detail) { + if (errorData.detail === "Invalid input data") { + friendlyMessage = "Revisa los valores del Soporte y Núcleo. Asegúrate de que el 'Inicio del Soporte' sea menor o igual al 'Fin del Soporte', y que el 'Núcleo' esté dentro del 'Soporte'."; + } else if (typeof errorData.detail === 'string') { + friendlyMessage = errorData.detail; + } + } else if (errorData.errors && Array.isArray(errorData.errors) && errorData.errors.length > 0) { + friendlyMessage = errorData.errors.map(err => { + let cleanMsg = err.msg ? err.msg.replace("Value error, ", "") : "Valor incorrecto"; + if (err.loc && err.loc.includes("levels")) { + const levelIndex = err.loc[err.loc.indexOf("levels") + 1]; + const termName = scaleKeys[levelIndex] || `Nivel ${Number(levelIndex) + 1}`; + return `• En la etiqueta "${termName}": ${cleanMsg}`; + } + return `• ${cleanMsg}`; + }).join("\n"); + } + + setSubmitError(friendlyMessage); + } finally { + setIsLoading(false); + } + }; + + // Petición para guardar en el historial + const handleSaveToHistory = async () => { + const token = localStorage.getItem('token'); + if (!token) { + alert("Para guardar tu modelo debes iniciar sesión primero. Puedes seguir visualizando la gráfica sin problema."); + return; + } + + const defaultName = criterionName ? `Modelo de ${criterionName}` : "Mi nueva gráfica DoC-IT2MF"; + const historyName = prompt("Dale un nombre a este modelo para guardarlo en tu historial:", defaultName); + + if (!historyName) return; + + setIsLoading(true); + try { + const payload = { + name: historyName, + results: finalResult.levels || finalResult.results + }; + + await saveToHistory(payload); + + alert("¡Gráfica guardada con éxito en tu historial!"); + + } catch (error) { + console.error("Error al guardar en el historial:", error); + alert("Hubo un problema al guardar el modelo: " + error); } finally { setIsLoading(false); } @@ -191,18 +243,22 @@ export default function DocEditor() { onBack={() => setStep(1)} subscales={subscales} onOpenSubscale={handleOpenSubscale} + submitError={submitError} /> )} {step === 3 && finalResult && ( -
- +
+
)} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 0000000..6947934 --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { getUserHistory, deleteHistoryItem } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; + +export default function History() { + const [historyItems, setHistoryItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + fetchHistory(); + }, []); + + const fetchHistory = async () => { + setIsLoading(true); + try { + const data = await getUserHistory(); + const items = Array.isArray(data) ? data : data.history || data.items || []; + + setHistoryItems(items.reverse()); + } catch (error) { + console.error("Error fetching history:", error); + alert("Hubo un problema al cargar el historial."); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('¿Seguro que quieres borrar este modelo definitivamente?')) return; + + try { + await deleteHistoryItem(id); + setHistoryItems(prev => prev.filter(item => item._id !== id && item.id !== id)); + if (expandedId === id) setExpandedId(null); + } catch (error) { + alert("Error al borrar: " + error); + } + }; + + const toggleExpand = (id) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+ {/* Cabecera */} +
+
+

Mi Historial

+

+ Aquí están todas las gráficas y modelos que has guardado. +

+
+ + + Nuevo Modelo + +
+ + {/* Lista de Historial */} + {isLoading ? ( +
+
+

Cargando tus gráficas...

+
+ ) : historyItems.length === 0 ? ( +
+ 📭 +

Aún no has guardado ningún modelo.

+

Ve al editor, crea una gráfica y dale a "Guardar".

+
+ ) : ( +
+ {historyItems.map((item) => { + const itemId = item._id || item.id; + const isExpanded = expandedId === itemId; + + return ( +
+ + {/* Cabecera de la Card */} +
+
+
+ 📊 +
+
+

{item.name || 'Modelo sin título'}

+

+ {item.created_at + ? `Guardado el ${new Date(item.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric' })}` + : 'Guardado en el historial' + } +

+
+
+ +
+ + +
+
+ + {/* Contenido Desplegable (La gráfica)*/} +
+
+ {isExpanded ? ( + + ) : ( +
+ )} +
+
+ +
+ ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..8f3ff7a --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,135 @@ +import { useState, useEffect, useRef } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; +import EyeIcon from '../components/EyeIcon'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + + const navigate = useNavigate(); + const { login } = useAuth(); + const [searchParams] = useSearchParams(); + + const googleLoginProcessed = useRef(false); + + useEffect(() => { + const token = searchParams.get('token'); + + if (token && !googleLoginProcessed.current) { + googleLoginProcessed.current = true; + + const url = new URL(window.location); + url.searchParams.delete('token'); + window.history.replaceState({}, '', url); + + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + + const decodedToken = JSON.parse(jsonPayload); + + const googleUser = { + _id: decodedToken.sub || decodedToken.user_id || "google_id", + username: decodedToken.email ? decodedToken.email.split('@')[0] : "Usuario Google", + email: decodedToken.email || "" + }; + + login({ user: googleUser, access_token: token }); + navigate('/', { replace: true }); + + } catch (err) { + console.error("Error al decodificar el token de Google:", err); + setTimeout(() => { + setError("Error al procesar el login con Google. El token está corrupto."); + }, 0); + } + } + }, [searchParams, login, navigate]); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + try { + const data = await authService.login(email, password); + login(data); + navigate('/'); + } catch (err) { + setError('Credenciales incorrectas.'); + } + }; + + const handleGoogleLogin = () => { + window.location.href = "http://localhost:8000/api/auth/google/login"; + }; + + return ( +
+
+ +
+

Deck of Cards

+

Accede a tu historial y gráficas guardadas

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="w-full px-5 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white" + placeholder="correo@ejemplo.com" + /> +
+ +
+ +
+ setPassword(e.target.value)} + className="w-full pl-5 pr-12 py-3 rounded-2xl border border-slate-200 focus:ring-2 focus:ring-blue-500 outline-none transition-all bg-slate-50 focus:bg-white" + placeholder="••••••••" + /> + +
+
+ + +
+ +
+
+
O
+
+ + + +

¿Nuevo por aquí? Crea una cuenta

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..cdc38fc --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; +import EyeIcon from '../components/EyeIcon'; + +export default function Register() { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + if (password !== confirmPassword) { + setError('Las contraseñas no coinciden. Por favor, revísalas.'); + return; + } + + try { + const data = await authService.register(username, email, password); + const userData = { id: data.user_id, username: username, email: email }; + login(userData, data.token); + navigate('/'); + } catch (err) { + setError(err.response?.data?.detail || 'Error al registrar el usuario.'); + } + }; + + return ( +
+
+ +
+

Crear Cuenta

+

Inicia sesión para guardar tu progreso

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="Ej: usuario99" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="••••••••" + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="••••••••" + /> + +
+
+ + +
+ +

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

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 5cc0534..3337675 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,16 +1,22 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '../components/layout/MainLayout'; import DocEditor from '../pages/DocEditor'; +import Login from '../pages/Login'; +import Register from '../pages/Register'; +import History from '../pages/History'; -export function AppRouter() { +export default function AppRouter() { return ( - - - }> - } /> - } /> - - - + + + + } /> + } /> + } /> + } /> + } /> + + + ); } \ No newline at end of file diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..f568a1d --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,18 @@ +import api from '../lib/api'; + +export const authService = { + login: async (email, password) => { + const response = await api.post('/auth/login', { email, password }); + return response.data; + }, + + register: async (username, email, password) => { + const response = await api.post('/auth/register', { username, email, password }); + return response.data; + }, + + getCurrentUser: async () => { + const response = await api.get('/auth/me'); + return response.data; + } +}; \ No newline at end of file diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js index 53c3cc8..56f9b66 100644 --- a/frontend/src/services/docService.js +++ b/frontend/src/services/docService.js @@ -5,19 +5,47 @@ export const calculateValueFunction = async (payload) => { const response = await api.post('/criteria/doc/value-function', payload); return response.data; } catch (error) { - console.error('Error calculating value function:', error); - throw error.response?.data?.detail || error.message; + if (error.response && error.response.data) throw error.response.data; + throw error; } }; export const buildFuzzyGraph = async (payload) => { try { - const response = await api.post('/criteria/doc-mf/build', payload); + const response = await api.post('/criteria/doc-it2mf/build', payload); return response.data; } catch (error) { - console.error('Error building fuzzy graph:', error); - throw error.response?.data?.detail || error.message; + if (error.response && error.response.data) throw error.response.data; + throw error; } +}; +export const saveToHistory = async (payload) => { + try { + const response = await api.post('/history/add', payload); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; +export const getUserHistory = async () => { + try { + const response = await api.get('/history/user'); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } +}; + +export const deleteHistoryItem = async (id) => { + try { + const response = await api.delete(`/history/${id}`); + return response.data; + } catch (error) { + if (error.response && error.response.data) throw error.response.data; + throw error; + } }; \ No newline at end of file