diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index 8412d36..ee9841e 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -3,14 +3,13 @@ import Controls from '../membershipFunction/Controls'; const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; -export default function Step2FuzzyModeling({ - baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack -}) { +export default function Step2FuzzyModeling({baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack, subscales, onOpenSubscale}) { const scaleKeys = Object.keys(baseScale); const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; return (
+

Paso 2: Modelar Conceptos Difusos

@@ -28,13 +27,27 @@ export default function Step2FuzzyModeling({ })}
- + - +
diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx new file mode 100644 index 0000000..70bb4ce --- /dev/null +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +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 handleAddCard = () => { + setCardsCount(prev => prev + 1); + setBlankCards([...blankCards, 0]); + }; + + const handleRemoveCard = () => { + if (cardsCount <= 2) return; + setCardsCount(prev => prev - 1); + setBlankCards(blankCards.slice(0, -1)); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlanks = [...blankCards]; + if (newBlanks[index] + delta >= 0) { + newBlanks[index] += delta; + setBlankCards(newBlanks); + } + }; + + const handleSave = () => { + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards }); + }; + + const handleDelete = () => { + onSave(targetInfo.term, targetInfo.side, null); + }; + + return ( +
+
+ +
+
+

Diseñar Subescala

+

+ Ajustando pendiente {targetInfo.side === 'left' ? 'Izquierda (Ascendente)' : 'Derecha (Descendente)'} del término "{targetInfo.term}" +

+
+ +
+ + {/* Tablero */} +
+
+ {Array.from({ length: cardsCount }).map((_, index) => ( + +
+
+ {cardsCount > 2 && index === cardsCount - 1 && ( + + )} + {index + 1} +
+
+ {index < cardsCount - 1 && ( + + )} +
+ ))} + +
+ +
+
+
+ + {/* Botones */} +
+ + +
+ + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index fe7e785..f879c4a 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -1,21 +1,18 @@ -export default function Controls({ selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf }) { +export default function Controls({ + selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf, + subscales, onOpenSubscale +}) { if (!selectedTerm || !currentMf) return null; const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + let absoluteMin = 0, absoluteMax = 1; + if (selectedIndex > 0) absoluteMin = mfDefinitions[scaleKeys[selectedIndex - 1]].coreEnd; + if (selectedIndex < scaleKeys.length - 1) absoluteMax = mfDefinitions[scaleKeys[selectedIndex + 1]].coreStart; - if (selectedIndex > 0) { - const prevTerm = scaleKeys[selectedIndex - 1]; - prevCoreEnd = mfDefinitions[prevTerm].coreEnd; - prevSupportEnd = mfDefinitions[prevTerm].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - const nextTerm = scaleKeys[selectedIndex + 1]; - nextCoreStart = mfDefinitions[nextTerm].coreStart; - nextSupportStart = mfDefinitions[nextTerm].supportStart; - } + const leftSubscale = subscales?.[selectedTerm]?.left; + const rightSubscale = subscales?.[selectedTerm]?.right; return (
@@ -24,34 +21,56 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS Ajustando: "{selectedTerm}" -
-
+
+ {/* Lado izquierdo (Pendiente ascendente) */} +
- updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
- updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
+ + {/* 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('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('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+ + {/* Botón subescala derecha */} +
+
diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 3baa1f2..34a24c9 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,109 +1,126 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; +import SubscaleModal from '../components/editor/SubscaleModal'; // <-- IMPORTAMOS EL MODAL import { calculateValueFunction } from '../services/docService'; export default function DocEditor() { - const [step, setStep] = useState(1); - const [isLoading, setIsLoading] = useState(false); + 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 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({}); + // ESTADOS: FASE 2 + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); - // 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); } }; + // ESTADOS: SUBESCALAS (FASE 2.5) + // Formato: { "regular": { left: { cardsCount: 3, blankCards: [1, 0] }, right: null }, "bueno": ... } + const [subscales, setSubscales] = useState({}); + const [modalTarget, setModalTarget] = useState(null); // { term: 'regular', side: 'left', initialData: {...} } - 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."); - } + // 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); } }; - 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); } - }; + 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."); + } - // 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; + 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); } + }; - 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; - } + // 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; - const anchor = baseScale[selectedTerm]; + 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; + } - 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; + const anchor = baseScale[selectedTerm]; - if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; - if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + 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 }; + 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; - } + 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 }; + }); + }; - return { ...prev, [selectedTerm]: current }; - }); - }; + // MANEJADORES: SUBESCALAS + const handleOpenSubscale = (term, side, initialData) => { + setModalTarget({ term, side, initialData }); + }; - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF:", { baseScale, mfDefinitions }); - alert("¡Mira la consola! JSON preparado."); - }; + const handleSaveSubscale = (term, side, data) => { + setSubscales(prev => ({ + ...prev, + [term]: { + ...prev[term], + [side]: data + } + })); + setModalTarget(null); + }; + + const handleFinalSubmit = () => { + console.log("PAYLOAD DOC-MF COMPLETO:", { baseScale, mfDefinitions, subscales }); + alert("JSON en consola."); + }; return (
@@ -122,6 +139,17 @@ export default function DocEditor() { selectedTerm={selectedTerm} setSelectedTerm={setSelectedTerm} updateCurrentMf={updateCurrentMf} handleFinalSubmit={handleFinalSubmit} onBack={() => setStep(1)} + subscales={subscales} + onOpenSubscale={handleOpenSubscale} + /> + )} + + {modalTarget && ( + setModalTarget(null)} + onSave={handleSaveSubscale} + targetInfo={modalTarget} /> )}