diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index eb155e3..fb0fca2 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, { useEffect, useRef, useState } from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/css/main.css b/frontend/src/css/main.css index a2d6067..18ce247 100644 --- a/frontend/src/css/main.css +++ b/frontend/src/css/main.css @@ -62,3 +62,117 @@ body { .introjs-prevbutton{ @apply bg-transparent border border-midnightBlueTheme-buttonColor text-midnightBlueTheme-buttonColor !important; } + +.road-intro-overlay { + background: + radial-gradient(circle at 20% 20%, rgba(36, 59, 116, 0.68), transparent 28%), + radial-gradient(circle at 80% 18%, rgba(122, 35, 56, 0.38), transparent 24%), + linear-gradient(180deg, rgba(26, 42, 82, 0.98) 0%, rgba(12, 18, 37, 0.99) 100%); + animation: roadIntroFade 2.6s ease forwards; +} + +.road-intro-panel { + animation: roadIntroLift 1.3s ease both; +} + +.road-intro-title { + animation: roadIntroGlow 1.8s ease both; +} + +.road-intro-road { + position: relative; + height: 92px; + border-radius: 9999px; + overflow: hidden; + background: linear-gradient(90deg, rgba(245, 242, 236, 0.06), rgba(245, 242, 236, 0.16), rgba(245, 242, 236, 0.06)); + border: 1px solid rgba(245, 242, 236, 0.14); +} + +.road-intro-road::before { + content: ''; + position: absolute; + inset: 14px 24px; + border-radius: 9999px; + border: 1px solid rgba(245, 242, 236, 0.08); +} + +.road-intro-road span { + position: absolute; + top: 50%; + width: 22%; + height: 6px; + border-radius: 9999px; + background: rgba(245, 242, 236, 0.85); + transform: translateY(-50%); + animation: roadIntroDrive 1.6s ease-in-out infinite; +} + +.road-intro-road span:nth-child(1) { + left: 10%; + animation-delay: 0s; +} + +.road-intro-road span:nth-child(2) { + left: 39%; + animation-delay: 0.15s; +} + +.road-intro-road span:nth-child(3) { + left: 68%; + animation-delay: 0.3s; +} + +@keyframes roadIntroDrive { + 0% { + opacity: 0.2; + transform: translateY(-50%) scaleX(0.86); + } + 50% { + opacity: 1; + transform: translateY(-50%) scaleX(1.08); + } + 100% { + opacity: 0.2; + transform: translateY(-50%) scaleX(0.86); + } +} + +@keyframes roadIntroGlow { + 0% { + opacity: 0; + transform: translateY(24px); + filter: blur(8px); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +@keyframes roadIntroLift { + 0% { + opacity: 0; + transform: translateY(28px) scale(0.98); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes roadIntroFade { + 0% { + opacity: 0; + } + 12% { + opacity: 1; + } + 88% { + opacity: 1; + } + 100% { + opacity: 0; + visibility: hidden; + } +} diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 2b3a420..43776d6 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -9,6 +9,7 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton import BaseIcon from "../components/BaseIcon"; import { getPageTitle } from '../config' import Link from "next/link"; +import BaseButton from '../components/BaseButton'; import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; @@ -16,6 +17,13 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; +type JourneyContext = { + roleId: string; + roleTitle: string; + city: string; + contractor: string; +}; + const Dashboard = () => { const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor); @@ -39,6 +47,7 @@ const Dashboard = () => { }); const { currentUser } = useAppSelector((state) => state.auth); const { isFetchingQuery } = useAppSelector((state) => state.openAi); + const [journeyContext, setJourneyContext] = React.useState(null); const { rolesWidgets, loading } = useAppSelector((state) => state.roles); @@ -82,6 +91,22 @@ const Dashboard = () => { if (!currentUser || !widgetsRole?.role?.value) return; getWidgets(widgetsRole?.role?.value || '').then(); }, [widgetsRole?.role?.value]); + + React.useEffect(() => { + const storedJourney = localStorage.getItem('roadTalentJourney'); + + if (!storedJourney) { + setJourneyContext(null); + return; + } + + try { + setJourneyContext(JSON.parse(storedJourney)); + } catch (error) { + console.error('Failed to parse road talent journey from storage', error); + setJourneyContext(null); + } + }, []); return ( <> @@ -97,6 +122,45 @@ const Dashboard = () => { main> {''} + + {journeyContext && ( +
+
+
+

Маршрут из публичного MVP

+

Вы вошли через сценарий «город → подрядчик → роль»

+

+ Контекст сохранён после публичной карты и теперь может использоваться как стартовая точка для наполнения справочников, проверки подрядчиков и настройки ролевых карточек. +

+
+
+
+

Город

+

{journeyContext.city}

+
+
+

Подрядчик

+

{journeyContext.contractor}

+
+
+

Роль

+

{journeyContext.roleTitle || 'Не выбрана'}

+
+
+
+
+ {hasPermission(currentUser, 'READ_CITIES') && ( + + )} + {hasPermission(currentUser, 'READ_CONTRACTORS') && ( + + )} + {hasPermission(currentUser, 'READ_ROLE_CARDS') && ( + + )} +
+
+ )} {hasPermission(currentUser, 'CREATE_ROLES') && state.style.linkColor); +type RoleCard = { + id: string; + title: string; + caption: string; + description: string; + accent: string; +}; - const title = 'Кадровый суверенитет Дороги' +type JourneyContext = { + roleId: string; + roleTitle: string; + city: string; + contractor: string; +}; - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); +const journeyStorageKey = 'roadTalentJourney'; - const imageBlock = (image) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
+const palette = { + navy: '#243B74', + midnight: '#1A2A52', + burgundy: '#56061D', + berry: '#7A2338', + paper: '#F5F2EC', +}; + +const cityMarkers: CityMarker[] = [ + { + id: 'moscow', + name: 'Москва', + district: 'Центральный кластер', + contractor: 'АО «ДорИнфраструктура»', + description: + 'Координирует набор инженерных команд, стажировки и быстрый вывод специалистов на федеральные дорожные объекты.', + specialization: 'Цифровое управление проектами и кадровый резерв', + top: '35%', + left: '18%', + popupTop: '18%', + popupLeft: '23%', + }, + { + id: 'saint-petersburg', + name: 'Санкт-Петербург', + district: 'Северо-Западный кластер', + contractor: 'ООО «Балтийские магистрали»', + description: + 'Развивает дорожное проектирование и практикум для молодых специалистов под задачи северо-западных регионов.', + specialization: 'Проектирование, BIM и подготовка мастеров участка', + top: '26%', + left: '16%', + popupTop: '8%', + popupLeft: '21%', + }, + { + id: 'kazan', + name: 'Казань', + district: 'Приволжский кластер', + contractor: 'ГК «Трасса Развития»', + description: + 'Собирает межвузовские команды для пилотных дорожных полигонов, подготовки механиков и специалистов по безопасности движения.', + specialization: 'Учебные полигоны и отраслевые акселераторы', + top: '43%', + left: '29%', + popupTop: '24%', + popupLeft: '34%', + }, + { + id: 'yekaterinburg', + name: 'Екатеринбург', + district: 'Уральский кластер', + contractor: 'АО «УралДорКадры»', + description: + 'Формирует региональный кадровый центр по линейному строительству, эксплуатации техники и управлению подрядом.', + specialization: 'Линейное строительство и эксплуатация дорожной техники', + top: '45%', + left: '42%', + popupTop: '26%', + popupLeft: '47%', + }, + { + id: 'novosibirsk', + name: 'Новосибирск', + district: 'Сибирский кластер', + contractor: 'ООО «СибАвтоДор»', + description: + 'Запускает маршруты переобучения, отраслевую аналитику и совместные программы для СПО и подрядчиков.', + specialization: 'Переобучение и аналитика по потребности в кадрах', + top: '50%', + left: '58%', + popupTop: '31%', + popupLeft: '62%', + }, + { + id: 'vladivostok', + name: 'Владивосток', + district: 'Дальневосточный кластер', + contractor: 'ГК «Восточный путь»', + description: + 'Курирует кадровые потоки для портовых подходов, мостовых объектов и дорожных узлов Дальнего Востока.', + specialization: 'Мостостроение, портовая логистика и восточные коридоры', + top: '54%', + left: '87%', + popupTop: '35%', + popupLeft: '69%', + }, +]; + +const roleCards: RoleCard[] = [ + { + id: 'school', + title: 'Школа', + caption: 'Ранняя профориентация', + description: 'Маршрут для учеников, наставников и школьных команд, которым нужен вход в отрасль через экскурсии и треки развития.', + accent: '#243B74', + }, + { + id: 'college', + title: 'СПО и ВО', + caption: 'Подготовка специалистов', + description: 'Для колледжей и вузов, которые синхронизируют образовательные программы с реальными дорожными проектами.', + accent: '#1A2A52', + }, + { + id: 'contractor', + title: 'Подрядные организации', + caption: 'Быстрый кадровый контур', + description: 'Для компаний, которым важно увидеть локальный кадровый резерв, потребности по ролям и точки входа в экосистему.', + accent: '#56061D', + }, + { + id: 'government', + title: 'Государственные институты', + caption: 'Управление системой', + description: 'Для операторов отрасли, которые управляют координацией партнёров, показателями и региональными соглашениями.', + accent: '#7A2338', + }, +]; + +function getRoleTitle(roleId: string) { + return roleCards.find((role) => role.id === roleId)?.title || ''; +} + +function RoleIllustration({ roleId }: { roleId: string }) { + const stroke = palette.paper; + + if (roleId === 'school') { + return ( + ); + } - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; + if (roleId === 'college') { + return ( + + ); + } + + if (roleId === 'contractor') { + return ( + + ); + } return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
- -
+ ); } -Starter.getLayout = function getLayout(page: ReactElement) { +export default function RoadTalentLanding() { + const router = useRouter(); + const [showIntro, setShowIntro] = React.useState(true); + const [selectedCity, setSelectedCity] = React.useState(cityMarkers[0]); + const [selectedRole, setSelectedRole] = React.useState(''); + const [roleStageVisible, setRoleStageVisible] = React.useState(false); + const [validationMessage, setValidationMessage] = React.useState(''); + + React.useEffect(() => { + const introTimer = window.setTimeout(() => { + setShowIntro(false); + }, 2600); + + return () => window.clearTimeout(introTimer); + }, []); + + const openRoleStage = () => { + if (!selectedCity) { + setValidationMessage('Сначала выберите город на карте.'); + return; + } + + setValidationMessage(''); + setRoleStageVisible(true); + window.setTimeout(() => { + document.getElementById('roles-stage')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 120); + }; + + const continueToLogin = () => { + if (!selectedCity) { + setValidationMessage('Сначала выберите город и подрядчика.'); + return; + } + + if (!selectedRole) { + setValidationMessage('Выберите роль, чтобы продолжить ко входу.'); + return; + } + + const roleTitle = getRoleTitle(selectedRole); + const journeyContext: JourneyContext = { + roleId: selectedRole, + roleTitle, + city: selectedCity.name, + contractor: selectedCity.contractor, + }; + + localStorage.setItem(journeyStorageKey, JSON.stringify(journeyContext)); + + router.push({ + pathname: '/login', + query: { + role: selectedRole, + city: selectedCity.name, + contractor: selectedCity.contractor, + }, + }); + }; + + return ( + <> + + {getPageTitle('Кадровый суверенитет дорожной отрасли')} + + +
+ {showIntro && ( +
+ +
+
+ + + +
+

кадровый суверенитет дорожной отрасли

+

+ Соединяем города, подрядчиков и роли в одном маршруте входа +

+

+ Интерактивный MVP-портал для выбора города, просмотра подрядчика и быстрого перехода к авторизации по нужной роли. +

+
+
+ )} + +
+
+ +
+
+
+

MVP-портал

+
+
+ +
+
+

Кадровый суверенитет

+

дорожной отрасли России

+
+
+
+ +
+ + + +
+
+ +
+
+
+ + Анимация входа → выбор города → роль → логин +
+

+ Выберите город на карте России и войдите в отраслевую экосистему с нужной ролью. +

+

+ Первый MVP-срез уже проводит пользователя по ключевому сценарию: показывает городские точки, раскрывает карточку подрядчика, + предлагает роль и передаёт контекст в авторизацию без лишних шагов. +

+ +
+ + +
+ +
+ {[ + { value: '6', label: 'городов в демо-карте' }, + { value: '4', label: 'роли входа' }, + { value: '1', label: 'сквозной маршрут MVP' }, + ].map((item) => ( +
+
{item.value}
+
{item.label}
+
+ ))} +
+
+ +
+
+
+

Маршрут пользователя

+

Первый экран, который продаёт логику сервиса

+
+
public MVP
+
+ +
+ {[ + '1. Анимированный вход знакомит пользователя с сервисом.', + '2. Карта России позволяет кликнуть по городам и увидеть подрядчика.', + '3. Выбор роли переносится в экран логина и дальше сохраняется для рабочего кабинета.', + ].map((item) => ( +
+
+

{item}

+
+ ))} +
+ +
+

что дальше в админке

+

+ После входа администратор уже может пользоваться существующими справочниками Cities, + Contractors и Role cards для наполнения отраслевых данных. +

+
+
+
+ +
+
+
+
+

Шаг 1

+

Интерактивная карта России по городам

+
+
+ Нажмите на город, чтобы открыть подрядчика +
+
+ +
+
+
Россия · дорожные кластеры
+
+ +
+
+ + + + + + + + + + + {selectedCity && ( +
+

Подрядчик

+

{selectedCity.contractor}

+

{selectedCity.description}

+
+ )} + + {cityMarkers.map((city) => { + const isActive = selectedCity?.id === city.id; + + return ( + + ); + })} +
+ +
+ {cityMarkers.map((city) => ( + + ))} +
+
+
+ + +
+ +
+
+
+

Шаг 2

+

Выберите роль входа

+

Четыре карточки роли отражают ключевых участников экосистемы: от школы до подрядных организаций и государственных институтов.

+
+
+ {selectedCity ? `Город: ${selectedCity.name}` : 'Сначала выберите город'} +
+
+ +
+ {roleCards.map((role) => { + const isActive = selectedRole === role.id; + + return ( + + ); + })} +
+ +
+
+

Шаг 3

+

Переход к логину и паролю

+

Выбранные город, подрядчик и роль будут показаны на экране логина и сохранятся для первого экрана внутри кабинета.

+
+
+
+

Город

+

{selectedCity?.name || 'Не выбран'}

+
+
+

Подрядчик

+

{selectedCity?.contractor || 'Ожидание выбора'}

+
+
+

Роль

+

{getRoleTitle(selectedRole) || 'Не выбрана'}

+
+
+
+ +
+

+ {selectedRole + ? `Готово: выбран маршрут «${selectedCity?.name || ''} → ${getRoleTitle(selectedRole)}».` + : 'Выберите одну из ролей, чтобы открыть следующий шаг.'} +

+
+ + +
+
+
+ +
+

© 2026 Кадровый суверенитет дорожной отрасли — первый публичный маршрут входа для партнёров и образовательных учреждений.

+
+ + +
+
+
+
+ + ); +} + +RoadTalentLanding.getLayout = function getLayout(page: ReactElement) { return {page}; }; - diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index f3a4f2f..40852af 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -1,273 +1,336 @@ - - -import React, { useEffect, useState } from 'react'; +import React from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; -import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import BaseIcon from "../components/BaseIcon"; -import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js'; -import SectionFullScreen from '../components/SectionFullScreen'; -import LayoutGuest from '../layouts/Guest'; +import Link from 'next/link'; import { Field, Form, Formik } from 'formik'; -import FormField from '../components/FormField'; -import FormCheckRadio from '../components/FormCheckRadio'; -import BaseDivider from '../components/BaseDivider'; +import { mdiArrowLeft, mdiEye, mdiEyeOff, mdiInformation, mdiMapMarker, mdiShieldAccount } from '@mdi/js'; +import { toast, ToastContainer } from 'react-toastify'; +import BaseButton from '../components/BaseButton'; import BaseButtons from '../components/BaseButtons'; -import { useRouter } from 'next/router'; +import BaseDivider from '../components/BaseDivider'; +import BaseIcon from '../components/BaseIcon'; +import CardBox from '../components/CardBox'; +import FormCheckRadio from '../components/FormCheckRadio'; +import FormField from '../components/FormField'; +import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; import { findMe, loginUser, resetAction } from '../stores/authSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; -import Link from 'next/link'; -import {toast, ToastContainer} from "react-toastify"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' +import { useRouter } from 'next/router'; + +type JourneyContext = { + roleId: string; + roleTitle: string; + city: string; + contractor: string; +}; + +const journeyStorageKey = 'roadTalentJourney'; + +const roleLabels: Record = { + school: 'Школа', + college: 'СПО и ВО', + contractor: 'Подрядные организации', + government: 'Государственные институты', +}; export default function Login() { const router = useRouter(); const dispatch = useAppDispatch(); const textColor = useAppSelector((state) => state.style.linkColor); const iconsColor = useAppSelector((state) => state.style.iconsColor); - const notify = (type, msg) => toast(msg, { type }); - const [ illustrationImage, setIllustrationImage ] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [ illustrationVideo, setIllustrationVideo ] = useState({video_files: []}) - const [contentType, setContentType] = useState('image'); - const [contentPosition, setContentPosition] = useState('left'); - const [showPassword, setShowPassword] = useState(false); - const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( - (state) => state.auth, - ); - const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com', + const notify = (type: 'success' | 'error', msg: string) => toast(msg, { type }); + const [showPassword, setShowPassword] = React.useState(false); + const [journeyContext, setJourneyContext] = React.useState(null); + const { currentUser, isFetching, errorMessage, token, notify: notifyState } = useAppSelector((state) => state.auth); + const [initialValues, setInitialValues] = React.useState({ + email: 'admin@flatlogic.com', password: '26ac88a4', - remember: true }) + remember: true, + }); - const title = 'Кадровый суверенитет Дороги' - - // Fetch Pexels image/video - useEffect( () => { - async function fetchData() { - const image = await getPexelsImage() - const video = await getPexelsVideo() - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); - // Fetch user data - useEffect(() => { + React.useEffect(() => { if (token) { dispatch(findMe()); } }, [token, dispatch]); - // Redirect to dashboard if user is logged in - useEffect(() => { + + React.useEffect(() => { if (currentUser?.id) { router.push('/dashboard'); } }, [currentUser?.id, router]); - // Show error message if there is one - useEffect(() => { - if (errorMessage){ - notify('error', errorMessage) + + React.useEffect(() => { + if (errorMessage) { + notify('error', errorMessage); + } + }, [errorMessage]); + + React.useEffect(() => { + if (notifyState?.showNotification) { + notify('success', notifyState?.textNotification); + dispatch(resetAction()); + } + }, [notifyState?.showNotification, notifyState?.textNotification, dispatch]); + + React.useEffect(() => { + if (!router.isReady) { + return; } - }, [errorMessage]) - // Show notification if there is one - useEffect(() => { - if (notifyState?.showNotification) { - notify('success', notifyState?.textNotification) - dispatch(resetAction()); - } - }, [notifyState?.showNotification]) + const roleFromQuery = typeof router.query.role === 'string' ? router.query.role : ''; + const cityFromQuery = typeof router.query.city === 'string' ? router.query.city : ''; + const contractorFromQuery = typeof router.query.contractor === 'string' ? router.query.contractor : ''; - const togglePasswordVisibility = () => { - setShowPassword(!showPassword); - }; + if (roleFromQuery || cityFromQuery || contractorFromQuery) { + const context = { + roleId: roleFromQuery, + roleTitle: roleLabels[roleFromQuery] || '', + city: cityFromQuery, + contractor: contractorFromQuery, + }; + setJourneyContext(context); + localStorage.setItem(journeyStorageKey, JSON.stringify(context)); + return; + } - const handleSubmit = async (value) => { - const {remember, ...rest} = value + const storedJourney = localStorage.getItem(journeyStorageKey); + + if (!storedJourney) { + setJourneyContext(null); + return; + } + + try { + const parsedJourney = JSON.parse(storedJourney) as JourneyContext; + setJourneyContext(parsedJourney); + } catch (error) { + console.error('Failed to parse road talent journey from storage', error); + setJourneyContext(null); + } + }, [router.isReady, router.query.city, router.query.contractor, router.query.role]); + + const handleSubmit = async (value: { email: string; password: string; remember: boolean }) => { + const { remember, ...rest } = value; await dispatch(loginUser(rest)); }; const setLogin = (target: HTMLElement) => { - setInitialValues(prev => ({ - ...prev, - email : target.innerText.trim(), - password: target.dataset.password ?? '', - })); + setInitialValues((prev) => ({ + ...prev, + email: target.innerText.trim(), + password: target.dataset.password ?? '', + })); }; - const imageBlock = (image) => ( - - ) - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; + const title = 'Кадровый суверенитет Дороги'; return ( -
- - {getPageTitle('Login')} - + <> + + {getPageTitle('Login')} + - -
- {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} - {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} -
- - - -

{title}

- -
-
- -

Use{' '} - setLogin(e.target)}>admin@flatlogic.com{' / '} - 26ac88a4{' / '} - to login as Admin

-

Use setLogin(e.target)}>client@hello.com{' / '} - 779ee45a8363{' / '} - to login as User

-
-
- -
+
+
+
+
+

вход в систему

+

{title}

+
+
+ + + Назад к карте + + +
+
+ +
+
+

контекст маршрута

+

Путь пользователя сохраняется до входа

+

+ Мы переносим выбранные на лендинге город, подрядчика и роль в экран авторизации, чтобы вход ощущался частью цельного сценария, а не отдельной формой. +

+ +
+ {journeyContext ? ( +
+
+
+

Выбранный маршрут

+

{journeyContext.roleTitle || 'Роль не выбрана'}

+
+
+ +
- - - - handleSubmit(values)} - > -
- - - -
- - - -
- -
-
+
+
+

Город

+

{journeyContext.city || 'Не выбран'}

+
+
+

Подрядчик

+

{journeyContext.contractor || 'Не выбран'}

+
+
-
- - - +

+ После авторизации этот контекст появится на обзорной странице и поможет быстро перейти к справочникам городов и подрядчиков в админ-интерфейсе. +

+
+ ) : ( +
+

Маршрут ещё не выбран

+

+ Вернитесь на карту России, чтобы выбрать город и роль. Вход доступен и без этого шага, но сценарий будет менее полным. +

+
+ )} - - Forgot password? - -
+
+
+
+ +
+
+

Демо-доступ для MVP

+

+ Для тестирования используйте стандартные аккаунты. Выбор роли на лендинге — это контекст пользовательского входа, а не замена существующей системной авторизации. +

+
+
+
+
+
- +
+ +
+
+

Демо-учётные записи

+

Быстрый вход

+

Подставьте готовую учётную запись кликом по email или введите собственные данные.

+
+
+ +
+
- - - -
-

- Don’t have an account yet?{' '} - - New Account - -

- - -
+
+
+

Администратор

+

+ setLogin(event.target as HTMLElement)} + > + admin@flatlogic.com + {' '} + / 26ac88a4 +

+
+
+

Пользователь

+

+ setLogin(event.target as HTMLElement)} + > + client@hello.com + {' '} + / 779ee45a8363 +

+
+
+ + + + handleSubmit(values)}> +
+ + + + +
+ + + + +
+ +
+ + + + + + Забыли пароль? + +
+ + + + + + +
+

+ Нет аккаунта?{' '} + + Создать новый + +

+ +
+
+
+
+ +
+

© 2026 {title}. Все права защищены.

+
+ + Privacy Policy + + + Вернуться на интерактивную карту +
- -
-

© 2026 {title}. © All rights reserved

- - Privacy Policy -
+ ); }