-
- Función de Valor: {result.criterion_name}
-
+
+ Función de Valor: {result.criterion_name}
+
-
-
- ({
- nombre: label,
- valor: value
- }))}
- margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
- >
-
-
-
- [value.toFixed(4), 'Valor DoC']}
- labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }}
- />
-
-
-
-
+
+
+ ({
+ nombre: label,
+ valor: value
+ }))}
+ margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
+ >
+
+
+
+ [value.toFixed(4), 'Valor DoC']}
+ labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }}
+ />
+
+
+
+
);
}
\ No newline at end of file
diff --git a/frontend/src/components/editor/Step1BaseScale.jsx b/frontend/src/components/editor/Step1BaseScale.jsx
new file mode 100644
index 0000000..115b450
--- /dev/null
+++ b/frontend/src/components/editor/Step1BaseScale.jsx
@@ -0,0 +1,92 @@
+import React, { useState, useEffect, useRef } from 'react';
+import CriterionInput from '../CriterionInput';
+import CardEditor from '../CardEditor';
+import BlankCardsCounter from '../BlankCardsCounter';
+import AddLevelButton from '../AddLevelButton';
+
+export default function Step1BaseScale({
+ criterionName, handleCriterionChange,
+ levels, handleLevelChange, handleAddLevel, handleRemoveLevel,
+ blankCards, handleBlankCardChange,
+ errors, handleGenerateBaseScale, isLoading
+}) {
+ const [isZoomActive, setIsZoomActive] = useState(true);
+ const containerRef = useRef(null);
+ const tableRef = useRef(null);
+ const [dimensions, setDimensions] = useState({ container: 1000, table: 0 });
+
+ useEffect(() => {
+ const updateMeasurements = () => {
+ if (containerRef.current && tableRef.current) {
+ setDimensions({
+ container: containerRef.current.offsetWidth,
+ table: tableRef.current.scrollWidth
+ });
+ }
+ };
+ const timeoutId = setTimeout(updateMeasurements, 50);
+ window.addEventListener('resize', updateMeasurements);
+ return () => {
+ clearTimeout(timeoutId);
+ window.removeEventListener('resize', updateMeasurements);
+ };
+ }, [levels, blankCards]);
+
+ const needsZoom = dimensions.table > dimensions.container;
+ const dynamicScale = needsZoom ? (dimensions.container / dimensions.table) * 0.95 : 1;
+ const currentScale = isZoomActive && needsZoom ? dynamicScale : 1;
+
+ return (
+ Cargando gráfico final...
;
+ }
+
+ return (
+
-
+
@@ -90,6 +121,7 @@ export default function BasicMode() {
handleLevelChange={handleLevelChange}
handleRemoveLevel={handleRemoveLevel}
totalLevels={levels.length}
+ error={errors.levels[index]}
/>
{index < levels.length - 1 && (
@@ -107,18 +139,18 @@ export default function BasicMode() {
-);
+ );
}
\ No newline at end of file
diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx
new file mode 100644
index 0000000..4cdc508
--- /dev/null
+++ b/frontend/src/pages/DocEditor.jsx
@@ -0,0 +1,220 @@
+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 Step3FinalGraph from '../components/editor/Step3FinalGraph';
+
+export default function DocEditor() {
+ const [step, setStep] = useState(1);
+ const [isLoading, setIsLoading] = useState(false);
+
+ // ESTADOS: FASE 1
+ const [criterionName, setCriterionName] = useState('');
+ const [levels, setLevels] = useState(['', '', '']);
+ const [blankCards, setBlankCards] = useState([0, 0]);
+ const [errors, setErrors] = useState({ criterion: false, levels: [] });
+
+ // ESTADOS: FASE 2
+ const [baseScale, setBaseScale] = useState({});
+ const [selectedTerm, setSelectedTerm] = useState(null);
+ const [mfDefinitions, setMfDefinitions] = useState({});
+ const [subscales, setSubscales] = useState({});
+ const [modalTarget, setModalTarget] = useState(null);
+
+ // ESTADO: FASE 3
+ const [finalResult, setFinalResult] = useState(null);
+
+ // MANEJADORES: FASE 1
+ 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]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); };
+ const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); };
+ const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); };
+ const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } };
+
+ const handleGenerateBaseScale = async () => {
+ const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) };
+ if (newErrors.criterion || newErrors.levels.includes(true)) {
+ setErrors(newErrors);
+ return alert("Por favor, rellena todos los campos.");
+ }
+
+ setIsLoading(true);
+ try {
+ const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } };
+ const baseResult = await calculateValueFunction(payloadBase);
+ setBaseScale(baseResult.values);
+ const initialMfs = {};
+ Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; });
+ setMfDefinitions(initialMfs);
+ setSelectedTerm(Object.keys(baseResult.values)[0]);
+ setStep(2);
+ } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); }
+ };
+
+ // MANEJADORES: FASE 2
+ const updateCurrentMf = (field, value) => {
+ if (!selectedTerm) return;
+ let numValue = parseFloat(value);
+ setMfDefinitions(prev => {
+ const scaleKeys = Object.keys(baseScale);
+ const selectedIndex = scaleKeys.indexOf(selectedTerm);
+ let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1;
+
+ if (selectedIndex > 0) {
+ prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd;
+ prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd;
+ }
+ if (selectedIndex < scaleKeys.length - 1) {
+ nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart;
+ nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart;
+ }
+
+ const anchor = baseScale[selectedTerm];
+
+ if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd;
+ if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd;
+ if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart;
+ if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart;
+ if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor;
+ if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor;
+
+ const current = { ...prev[selectedTerm], [field]: numValue };
+
+ if (field === 'supportStart') {
+ if (current.supportStart > current.coreStart) current.coreStart = current.supportStart;
+ if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart;
+ if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
+ } else if (field === 'coreStart') {
+ if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
+ if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart;
+ if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
+ } else if (field === 'coreEnd') {
+ if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd;
+ if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd;
+ if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
+ } else if (field === 'supportEnd') {
+ if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd;
+ if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd;
+ if (current.coreStart < current.supportStart) current.supportStart = current.coreStart;
+ }
+ return { ...prev, [selectedTerm]: current };
+ });
+ };
+
+ const handleOpenSubscale = (term, side, initialData) => {
+ setModalTarget({ term, side, initialData });
+ };
+
+ const handleSaveSubscale = (term, side, data) => {
+ setSubscales(prev => ({
+ ...prev,
+ [term]: {
+ ...prev[term],
+ [side]: data
+ }
+ }));
+ setModalTarget(null);
+ };
+
+ // Petición para el endpoint "build"
+ const handleFinalSubmit = async () => {
+ const scaleKeys = Object.keys(baseScale);
+
+ const payload = {
+ levels: scaleKeys.map(term => {
+ const mf = mfDefinitions[term];
+ const sub = subscales[term] || {};
+
+ const c_start = Number(mf.coreStart.toFixed(4));
+ const c_end = Number(mf.coreEnd.toFixed(4));
+
+ const s_start = Math.min(Number(mf.supportStart.toFixed(4)), c_start);
+ const s_end = Math.max(Number(mf.supportEnd.toFixed(4)), c_end);
+
+ const leftCount = sub.left ? sub.left.cardsCount : 2;
+ const left_nodes_x = Array.from({ length: leftCount }).map((_, i) =>
+ Number((s_start + (c_start - s_start) * (i / (leftCount - 1))).toFixed(4))
+ );
+
+ const rightCount = sub.right ? sub.right.cardsCount : 2;
+ const right_nodes_x = Array.from({ length: rightCount }).map((_, i) =>
+ Number((c_end + (s_end - c_end) * (i / (rightCount - 1))).toFixed(4))
+ );
+
+ return {
+ term: term,
+ core: [ c_start, c_end ],
+ support: [ s_start, s_end ],
+ left_nodes_x: left_nodes_x,
+ left_blank_cards: sub.left ? sub.left.blankCards : [0],
+ right_nodes_x: right_nodes_x,
+ right_blank_cards: sub.right ? sub.right.blankCards : [0]
+ };
+ })
+ };
+
+ 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));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ {step === 1 && (
+
+ )}
+
+ {step === 2 && (
+
setStep(1)}
+ subscales={subscales}
+ onOpenSubscale={handleOpenSubscale}
+ />
+ )}
+
+ {step === 3 && finalResult && (
+
+
+
+
+
+ )}
+
+ {modalTarget && (
+ setModalTarget(null)}
+ onSave={handleSaveSubscale}
+ targetInfo={modalTarget}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx
new file mode 100644
index 0000000..5cc0534
--- /dev/null
+++ b/frontend/src/routers/AppRouter.jsx
@@ -0,0 +1,16 @@
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import MainLayout from '../components/layout/MainLayout';
+import DocEditor from '../pages/DocEditor';
+
+export function AppRouter() {
+ return (
+
+
+ }>
+ } />
+ } />
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js
index 79150d3..53c3cc8 100644
--- a/frontend/src/services/docService.js
+++ b/frontend/src/services/docService.js
@@ -1,11 +1,23 @@
-import axios from '../lib/axios';
+import api from '../lib/api';
export const calculateValueFunction = async (payload) => {
try {
- const response = await axios.post('/criteria/doc/value-function', payload);
- return response.data;
+ const response = await api.post('/criteria/doc/value-function', payload);
+ return response.data;
} catch (error) {
- console.error("Error en calculateValueFunction:", error);
- throw error;
+ console.error('Error calculating value function:', error);
+ throw error.response?.data?.detail || error.message;
}
+};
+
+export const buildFuzzyGraph = async (payload) => {
+ try {
+ const response = await api.post('/criteria/doc-mf/build', payload);
+ return response.data;
+ } catch (error) {
+ console.error('Error building fuzzy graph:', error);
+ throw error.response?.data?.detail || error.message;
+ }
+
+
};
\ No newline at end of file