// shell.jsx — Sidebar, TopBar, Login, Tenant picker — using DS classes verbatim function Sidebar({ route, onNavigate, alertCount = 3, taskCount = 14, tenant, user, role = "", onLogout, collapsed = false, onToggleCollapse, }) { // Primary nav items are grouped by domain. // Sub-pages (site, hives, telemetry) and advanced screens (rules, commands, // ota, recommendations) are reached contextually, not from the sidebar. const NAV = [ { group: "Объект", items: [ { id:"overview", icon:"ri-dashboard-2-line", label:"Обзор" }, { id:"sites", icon:"ri-leaf-line", label:"Теплицы" }, { id:"devices", icon:"ri-router-line", label:"Устройства" }, { id:"telemetry", icon:"ri-pulse-line", label:"Телеметрия" }, { id:"microclimate", icon:"ri-temp-hot-line", label:"Микроклимат" }, ]}, { group: "Работа", items: [ { id:"alerts", icon:"ri-alarm-warning-line", label:"Уведомления", badge: alertCount, badgeKind:"crit" }, { id:"tasks", icon:"ri-task-line", label:"Задачи", badge: taskCount, badgeKind:"warn" }, { id:"commands", icon:"ri-command-line", label:"Команды", roleMin:"operator" }, { id:"ota", icon:"ri-upload-cloud-2-line", label:"OTA", roleMin:"operator" }, { id:"rules", icon:"ri-git-branch-line", label:"Правила", roleMin:"operator" }, { id:"reports", icon:"ri-bar-chart-2-line", label:"Отчёты" }, { id:"roi", icon:"ri-funds-line", label:"ROI" }, ]}, { group: "Настройки", items: [ { id:"settings", icon:"ri-settings-3-line", label:"Настройки" }, { id:"simulator", icon:"ri-cpu-line", label:"Виртуальный стенд" }, ]}, ]; // [TASK_START:T014] const normalizedRole = String(role || "").toLowerCase(); const isLimitedOperator = normalizedRole === "worker" || normalizedRole === "technician"; const visibleNav = NAV.map(group => ({ ...group, items: group.items.filter(item => !(item.roleMin === "operator" && isLimitedOperator)), })); // [TASK_COMPLETE:T014] return ( ); } function TopBar({ route, onSearch }) { // Derive the breadcrumb trail from the current route ID using the // centralised breadcrumb map. Falls back to ["Ahal Agro Park"] for // unknown routes (matches Keeper.breadcrumbs fallback behaviour). const crumbs = Keeper.breadcrumbs.get(route); return (
{crumbs.map((c, i) => ( {i > 0 && } {i === crumbs.length - 1 ? {c} : {c}} ))}
); } /* Login & tenant picker */ function Login({ onLogin, sessionMessage = "" }) { const [step, setStep] = React.useState("creds"); // creds | tenants // [TASK_START:T010] const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const [remember, setRemember] = React.useState(true); const [loading, setLoading] = React.useState(false); const [formError, setFormError] = React.useState(""); const [fieldErrors, setFieldErrors] = React.useState({ email: "", password: "" }); // [TASK_COMPLETE:T010] const [user, setUser] = React.useState(null); const [tenants, setTenants] = React.useState([]); // [TASK_START:T011] function validate() { const errors = { email: "", password: "" }; if (!email || !email.trim() || !email.includes("@")) { errors.email = "Введите корректный адрес почты"; } if (!password || !password.trim()) { errors.password = "Введите пароль"; } setFieldErrors(errors); return !errors.email && !errors.password; } // [TASK_COMPLETE:T011] async function handleLoginSubmit(event) { event.preventDefault(); setFormError(""); // [TASK_START:T012] if (!validate()) return; setLoading(true); try { const response = await Keeper.api.login(email.trim(), password); const availableTenants = Array.isArray(response.tenants) ? response.tenants : []; setUser(response.user || null); setTenants(availableTenants); window.DATA = Object.assign({}, window.DATA || {}, { user: response.user || null, tenants: availableTenants }); setStep("tenants"); } catch (error) { const fallbackMessage = error instanceof Keeper.ApiError && (error.status === 401 || error.status === 403) ? "Неверный адрес почты или пароль" : "Нет соединения с сервером"; const formatted = typeof Keeper.formatApiFailure === "function" ? Keeper.formatApiFailure(error, { fallbackMessage }) : fallbackMessage; setFormError(formatted); } finally { setLoading(false); } // [TASK_COMPLETE:T012] } // [TASK_START:T013] async function handleTenantSelect(selectedTenant) { setLoading(true); setFormError(""); try { const selected = await Keeper.api.selectTenant(selectedTenant.id); const activeTenant = selected.activeTenant || selected.active_tenant || selectedTenant; if (remember) { localStorage.setItem("keeper.activeTenant", JSON.stringify(activeTenant)); } else { localStorage.removeItem("keeper.activeTenant"); } window.DATA = Object.assign({}, window.DATA || {}, { activeTenant, user: user || window.DATA?.user || null }); onLogin({ user: user || window.DATA?.user || null, tenant: activeTenant }); } catch (error) { const fallbackMessage = "Не удалось выбрать объект"; const formatted = typeof Keeper.formatApiFailure === "function" ? Keeper.formatApiFailure(error, { fallbackMessage }) : fallbackMessage; setFormError(formatted); } finally { setLoading(false); } } // [TASK_COMPLETE:T013] return (
🐝
Keeper
{step === "creds" && (

Войти в систему

Платформа мониторинга теплиц и опыления
{sessionMessage && (
{sessionMessage}
)}
setEmail(e.target.value)} /> {fieldErrors.email &&
{fieldErrors.email}
}
setPassword(e.target.value)} /> {fieldErrors.password &&
{fieldErrors.password}
}
Забыли пароль?
{formError &&
{formError}
}
или войти через SSO
© 2026 Keeper IIoT · v3.4.2 · конфиденциальность
)} {step === "tenants" && (

Выберите объект

Вам доступны {tenants.length} организации
{tenants.map(t => (
!loading && handleTenantSelect(t)}> {t.name}
{t.country} · {t.greenhouses} теплиц · {t.devices} устройств
{t.role}
))}
{formError &&
{formError}
}
)}

Видеть пасеку и теплицу как один организм

Активность колоний, состояние субстрата, прошивки шлейфов — на одном экране, в реальном времени, с автоматизацией для ваших агрономов.

98,4 %
Аптайм
312
Устройств онлайн
76 %
Активность ульев
); } window.Sidebar = Sidebar; window.TopBar = TopBar; window.Login = Login;