// CustomCalc — let the user build their own simple calculator. // Variables + formula + optional bands → saved in localStorage. const { useState: uxS, useMemo: uxM, useEffect: uxE } = React; const CC_KEY = 'cyna_custom_calcs_v1'; const newCalc = () => ({ id: 'c_' + Date.now().toString(36), nombre: 'Mi calculadora', descripcion: '', unidad: '', formula: 'peso * 0.5', variables: [ { id: 'peso', label: 'Peso', unit: 'kg', value: 70 }, ], bandas: [ // { hasta: Infinity, label: 'Normal', color: '#6E8A4C' } ], }); function loadCalcs() { try { const raw = localStorage.getItem(CC_KEY); if (raw) return JSON.parse(raw); } catch {} return [ { id: 'demo1', nombre: 'Dosis por kilo', descripcion: 'Dosis total a administrar según peso y dosis por kg.', unidad: 'mg', formula: 'peso * dosis', variables: [ { id: 'peso', label: 'Peso', unit: 'kg', value: 70 }, { id: 'dosis', label: 'Dosis por kg', unit: 'mg/kg', value: 10 }, ], bandas: [], }, ]; } function saveCalcs(arr) { try { localStorage.setItem(CC_KEY, JSON.stringify(arr)); } catch {} } // Safe-ish evaluator: allow only digits, operators, parens, dots, spaces and known variable ids. function compileFormula(formula, varIds) { const idSet = new Set(varIds); // Tokenize: numbers, identifiers, operators, parens. const tokens = formula.match(/[A-Za-z_][A-Za-z0-9_]*|\d+\.?\d*|[+\-*/().%,]|\*\*|\s+/g) || []; for (const t of tokens) { if (/^\s+$/.test(t)) continue; if (/^[A-Za-z_]/.test(t)) { if (!idSet.has(t) && !['Math', 'min', 'max', 'sqrt', 'pow', 'abs', 'round', 'floor', 'ceil', 'log'].includes(t)) { return { error: `Variable desconocida: ${t}` }; } } else if (!/^(\d+\.?\d*|[+\-*/().%,]|\*\*)$/.test(t)) { return { error: `Token no permitido: ${t}` }; } } try { // eslint-disable-next-line no-new-func const fn = new Function(...varIds, 'Math', 'min', 'max', 'sqrt', 'pow', 'abs', 'round', 'floor', 'ceil', 'log', `"use strict"; return (${formula});`); return { fn }; } catch (e) { return { error: 'Sintaxis inválida' }; } } function evalCalc(calc) { const ids = calc.variables.map(v => v.id); const { fn, error } = compileFormula(calc.formula, ids); if (error) return { error }; try { const args = calc.variables.map(v => +v.value || 0); const r = fn(...args, Math, Math.min, Math.max, Math.sqrt, Math.pow, Math.abs, Math.round, Math.floor, Math.ceil, Math.log); return { value: r }; } catch (e) { return { error: 'No se pudo calcular' }; } } function slug(s) { return s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '') || 'var'; } function CustomCalc() { const [list, setList] = uxS(loadCalcs); const [activeId, setActiveId] = uxS(() => loadCalcs()[0]?.id || null); const [editing, setEditing] = uxS(false); uxE(() => { saveCalcs(list); }, [list]); const active = list.find(c => c.id === activeId) || list[0]; const update = (patch) => setList(list.map(c => c.id === active.id ? { ...c, ...patch } : c)); const remove = (id) => { if (!confirm('¿Eliminar esta calculadora?')) return; const next = list.filter(c => c.id !== id); setList(next); if (next.length) setActiveId(next[0].id); }; const create = () => { const c = newCalc(); const next = [...list, c]; setList(next); setActiveId(c.id); setEditing(true); }; if (!active) { return (

Aún no tienes calculadoras

Crea la primera con tus propias variables y fórmula.

); } return (
{editing ? { update(c); setEditing(false); }} onCancel={() => setEditing(false)} onDelete={() => remove(active.id)}/> : setEditing(true)} onChange={update}/> }
); } // ─── RUN view (use the calculator) ───────────────────────── function CustomCalcRun({ calc, onEdit, onChange }) { const { value, error } = uxM(() => evalCalc(calc), [calc]); const klass = !error && isFinite(value) ? (calc.bandas || []).find(b => value <= (b.hasta ?? Infinity)) : null; const setVar = (i, patch) => { const next = calc.variables.map((v, j) => j === i ? { ...v, ...patch } : v); onChange({ variables: next }); }; return (

{calc.nombre || 'Calculadora'}

{calc.descripcion &&

{calc.descripcion}

}

Fórmula: {calc.formula}

{calc.variables.map((v, i) => (
setVar(i, { value: e.target.value === '' ? '' : +e.target.value })} step="any" /> {v.unit && {v.unit}}
))}
{error ? (
⚠️ Error en la fórmula: {error}
Revisa los nombres de tus variables y operadores.
) : ( <>
Resultado {isFinite(value) ? (Math.abs(value) < 0.01 || Math.abs(value) >= 10000 ? (+value).toExponential(3) : (+value).toFixed(2)) : '—'} {calc.unidad && {calc.unidad}}
{klass && (
Clasificación
{klass.label}
)} )}
); } // ─── EDIT view (configure variables + formula + bands) ───── function CustomCalcEditor({ calc, onSave, onCancel, onDelete }) { const [draft, setDraft] = uxS(calc); const upd = (patch) => setDraft(d => ({ ...d, ...patch })); const updVar = (i, patch) => upd({ variables: draft.variables.map((v, j) => j === i ? { ...v, ...patch } : v) }); const addVar = () => { let base = 'var', n = 1; while (draft.variables.find(v => v.id === base + n)) n++; upd({ variables: [...draft.variables, { id: base + n, label: 'Variable ' + n, unit: '', value: 0 }] }); }; const rmVar = (i) => upd({ variables: draft.variables.filter((_, j) => j !== i) }); const addBand = () => upd({ bandas: [...(draft.bandas || []), { hasta: 0, label: 'Banda', color: '#B97D3D', tint: '#F2E1D2' }] }); const updBand = (i, patch) => upd({ bandas: draft.bandas.map((b, j) => j === i ? { ...b, ...patch } : b) }); const rmBand = (i) => upd({ bandas: draft.bandas.filter((_, j) => j !== i) }); const test = uxM(() => evalCalc(draft), [draft]); return (

Editar calculadora

Configura los campos que el usuario verá y la fórmula a aplicar.

Información general
upd({ nombre: e.target.value })}/>
upd({ unidad: e.target.value })} placeholder="ej. ml/h, mg, %"/>
upd({ descripcion: e.target.value })} placeholder="Para qué sirve esta fórmula"/>
Variables
{draft.variables.map((v, i) => (
updVar(i, { label: e.target.value })}/>
updVar(i, { id: slug(e.target.value) })} className="mono" />
updVar(i, { unit: e.target.value })} placeholder="kg, ml, °C…"/>
updVar(i, { value: +e.target.value || 0 })} step="any"/>
))}
Fórmula