/* ============================================ Admin — Login + Shell + Dashboard ============================================ */ /* ============================================ Login ============================================ */ function Login({ onLogin, onBack, resetToken, resetEmail }) { // step: "login" | "forgot" | "reset" | "reset-ok" const [step, setStep] = useState(() => resetToken ? "reset" : "login"); const [email, setEmail] = useState(resetEmail || "admin@msamoro.com"); const [pass, setPass] = useState(""); const [passConf, setPassConf]= useState(""); const [err, setErr] = useState(""); const [msg, setMsg] = useState(""); const [loading, setLoading] = useState(false); const submitLogin = async (e) => { e.preventDefault(); setErr(""); setLoading(true); const r = await window.API.login(email, pass); if (r.ok) { onLogin(r.user); } else { setErr(r.error || "Credenciales incorrectas."); setLoading(false); } }; const submitForgot = async (e) => { e.preventDefault(); setErr(""); setLoading(true); const r = await window.API.forgotPassword(email); setLoading(false); if (r.ok) setMsg(r.mensaje); else setErr(r.error); }; const submitReset = async (e) => { e.preventDefault(); if (pass !== passConf) { setErr("Las contraseñas no coinciden."); return; } setErr(""); setLoading(true); const r = await window.API.resetPassword(resetToken, email, pass, passConf); setLoading(false); if (r.ok) { setStep("reset-ok"); setMsg(r.mensaje); } else setErr(r.error); }; const Left = () => (
MSA Moro
Producir alfalfa premium no es solo plantar — es medir, registrar y mejorar cada lote, año tras año.
— Hermanos Bertoldi, fundadores
Panel de gestión interna · v2.4.1 · campaña 2025/26
); const ErrMsg = () => err ? (
{err}
) : null; if (step === "forgot") return (
Recuperar acceso

¿Olvidaste tu
contraseña?

Te enviamos un enlace para restablecerla.

{msg ? (
{msg}
) : (
setEmail(e.target.value)} autoFocus required />
)}
); if (step === "reset") return (
Nueva contraseña

Restablecer
contraseña.

Ingresá tu nueva contraseña para {email}.

setPass(e.target.value)} autoFocus required minLength={8} placeholder="Mínimo 8 caracteres" />
setPassConf(e.target.value)} required minLength={8} />
); if (step === "reset-ok") return (
Listo

Contraseña
actualizada.

{msg}
); // step === "login" return (
Acceso exclusivo

Bienvenido
al panel directivo.

Acceso exclusivo para directivos y administración.

setEmail(e.target.value)} autoFocus />
setPass(e.target.value)} />
{onBack && ( )}
); } /* ============================================ Sidebar ============================================ */ function Sidebar({ active, onNav, onExit, onLogout, open, onClose }) { const items = [ { section: "Principal" }, { id: "dashboard", label: "Inicio", icon: "LayoutDashboard" }, { id: "produccion", label: "Producción", icon: "Sprout" }, { id: "stock", label: "Stock de fardos", icon: "Package", badge: "12.8k" }, { section: "Comercial" }, { id: "ventas", label: "Ventas y clientes", icon: "Receipt" }, { id: "asociacion", label: "Asociación", icon: "Handshake" }, { id: "terceros", label: "Servicios a terceros", icon: "Tractor" }, { section: "Finanzas" }, { id: "gastos", label: "Gastos y costos", icon: "Wallet" }, { id: "cheques", label: "Cheques", icon: "ScrollText", badge: "22" }, { id: "rent", label: "Rentabilidad", icon: "TrendingUp" }, { section: "Operaciones" }, { id: "logistica", label: "Logística", icon: "Truck", badge: String(window.DATA?.VIAJES?.filter(v => v.estado === "programado" || v.estado === "en curso").length ?? 0) }, { id: "empleados", label: "Empleados", icon: "Users", badge: "3" }, { id: "maquinaria", label: "Maquinaria", icon: "Wrench", badge: "8" }, ]; const handleNav = (id) => { onNav(id); onClose && onClose(); }; return ( <>
); } /* ============================================ Topbar ============================================ */ function NotifDropdown({ notifs, onNav, onClose }) { const ref = React.useRef(null); useEffect(() => { const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [onClose]); const iconMap = { cheque:"ScrollText", maquinaria:"Wrench", viaje:"Truck", gasto:"Wallet", stock:"Package", corte:"Scissors" }; const moduloMap = { produccion:"produccion", cheques:"cheques", maquinaria:"maquinaria", logistica:"logistica", gastos:"gastos", stock:"stock" }; const colorUrg = (u) => u === "alta" ? "var(--bad)" : u === "media" ? "#d97706" : "var(--admin-mute)"; const labelUrg = (u) => u === "alta" ? "Urgente" : u === "media" ? "Importante" : "Info"; const fmtFecha = (f) => { if (!f) return null; const d = new Date(f + "T00:00:00"); return d.toLocaleDateString("es-AR", { day:"2-digit", month:"short" }); }; return (
Notificaciones {notifs.length > 0 ? {notifs.length} : Sin alertas }
{notifs.length === 0 ? (
Todo al día
) : notifs.map(n => ( ))}
); } function Topbar({ user, onMenuClick, notificaciones = [], onNav }) { const nombre = user?.nombre || "Admin"; const initials = nombre.split(" ").map(w => w[0]).slice(0, 2).join("").toUpperCase(); const [showNotif, setShowNotif] = useState(false); const count = notificaciones.length; return (
⌘K
{showNotif && setShowNotif(false)} />}
{initials}
{nombre}
{user?.email || ""}
); } /* ============================================ Recharts shorthand ============================================ */ const RC = window.Recharts; function ChartCard({ title, sub, right, height = 260, children }) { return (
{title}
{sub &&
{sub}
}
{right}
{children}
); } /* ============================================ Dashboard ============================================ */ function Dashboard() { const D = window.DATA; const [stats, setStats] = useState(null); const [alerts, setAlerts] = useState([]); useEffect(() => { window.API.get("/dashboard").then(data => { if (!data.error) setStats(data); }); window.API.get("/notificaciones").then(data => { if (Array.isArray(data)) setAlerts(data); }); }, []); const ha = stats ? Number(stats.lotes.hectareas).toLocaleString("es-AR") : "…"; const lotesN = stats ? stats.lotes.total : "…"; const fardos = stats ? Number(stats.stock.total_fardos).toLocaleString("es-AR") : "…"; const clientes= stats ? stats.clientes.total : "…"; const empN = stats ? stats.empleados.total : "…"; const viajesP = stats ? stats.viajes.programados + stats.viajes.en_curso : "…"; const ventasMes = stats ? D.fmtARS(stats.ventas_mes) : "…"; const alertIcon = { cheque:"Clock", maquinaria:"Wrench", viaje:"Truck", gasto:"DollarSign", stock:"Package", corte:"Sprout" }; const alertClass = { alta:"bad", media:"warn", baja:"info" }; const ventasMensuales = stats?.ventas_mensuales || []; const produccionLotes = stats?.produccion_lotes || []; const ventasPorTipo = stats?.ventas_por_tipo || []; const ultimasVentas = stats?.ultimas_ventas || []; return (
Buen día. Resumen actualizado.
Datos en tiempo real · Campaña 2025/26
{/* KPIs */}
{/* Ventas mensuales + Alertas */}
{ventasMensuales.length === 0 ?
Sin datos de ventas aún
: `$${(v/1e6).toFixed(0)}M`} width={50} /> [D.fmtARS(v), "Ventas"]} /> }
Alertas
{alerts.length > 0 && {alerts.length} pendientes}
{alerts.length === 0 ?
Sin alertas activas
: alerts.slice(0, 6).map(a => (
window.navTo(a.modulo)}>
{a.titulo}
{a.detalle}
)) }
{/* Producción por lote + Ventas por producto */}
{lotesN} lotes} height={280} > {produccionLotes.length === 0 ?
Sin cortes registrados
: `${v}t`} width={42} /> [`${v} t`, "Producción"]} /> }
{ventasPorTipo.length === 0 ?
Sin ventas registradas
: {ventasPorTipo.map((e, i) => )} [`${v}%`, ""]} /> }
{/* Últimas ventas */}
Últimas ventas
{ultimasVentas.length === 0 ?
Sin ventas registradas
: {ultimasVentas.map((v, i) => ( ))}
FechaClienteProductoCantidadMontoEstado
{v.fecha} {v.cliente} {v.producto} {v.cant} {D.fmtARS(v.monto)}
}
); } function Kpi({ icon, label, value, delta, deltaDir, hint }) { const dirClass = deltaDir === "up" ? "up" : deltaDir === "down" ? "down" : "flat"; const arrow = deltaDir === "up" ? "ArrowUp" : deltaDir === "down" ? "ArrowDown" : null; return (
{delta && (
{arrow && } {delta}
)}
{label}
{value}
{hint &&
{hint}
}
); } function EstadoBadge({ estado }) { const map = { pagado: ["b-ok", "Pagado"], parcial: ["b-warn", "Parcial"], pendiente: ["b-bad", "Pendiente"], cobrado: ["b-ok", "Cobrado"], activo: ["b-ok", "Activo"], proximo: ["b-warn", "Próximo corte"], descanso: ["b-mute", "En descanso"], implantacion: ["b-info", "Implantación"], operativo: ["b-ok", "Operativo"], service: ["b-bad", "Service"], }; const [cls, label] = map[estado] || ["b-mute", estado]; return {label}; } Object.assign(window, { Login, Sidebar, Topbar, Dashboard, Kpi, EstadoBadge, ChartCard, RC });