/* ============================================
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 = () => (
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}
) : (
)}
);
if (step === "reset") return (
Nueva contraseña
Restablecer
contraseña.
Ingresá tu nueva contraseña para {email}.
);
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.
);
}
/* ============================================
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 ? (
) : 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 (
);
}
/* ============================================
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 => (
))
}
{/* 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
:
| Fecha | Cliente | Producto | Cantidad | Monto | Estado |
{ultimasVentas.map((v, i) => (
| {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 });