From 7bc9ddb8981f93f0713eb6b0617e4d8d57a09f32 Mon Sep 17 00:00:00 2001 From: boa604 Date: Fri, 27 Feb 2026 13:56:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B2=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8=20=D0=BD=D0=B0=20=D1=8D=D0=BA?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B5=20=D0=B0=D1=83=D1=82=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=9D?= =?UTF-8?q?=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D1=8B=D0=B9=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D0=BA=D0=B0=D0=BD=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=B0=D0=BC=D0=B5=D1=80=D1=83=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/app/src/main/AndroidManifest.xml | 1 + rn/app/android/gradle.properties | 3 + rn/app/ios/app/Info.plist | 2 + rn/app/package.json | 9 +- rn/app/src/components/common/Backdrop.js | 41 ---- .../components/inspections/InspectionItem.js | 36 --- .../components/inspections/InspectionList.js | 68 ------ .../src/components/layout/AppAuthProvider.js | 58 ++--- rn/app/src/components/layout/AppHeader.js | 6 +- .../layout/AppPreTripInspectionsProvider.js | 63 ------ rn/app/src/components/layout/AppRoot.js | 4 + rn/app/src/components/layout/AppShell.js | 9 + .../src/components/scanner/BarcodeScanner.js | 32 +++ .../scanner/BarcodeScannerNative.js | 104 +++++++++ .../components/scanner/BarcodeScannerWeb.js | 37 +++ .../src/components/scanner/ScanResultModal.js | 84 +++++++ rn/app/src/components/scanner/ScannerArea.js | 89 ++++++++ .../components/scanner/ScannerPlaceholder.js | 39 ++++ rn/app/src/config/authConfig.js | 4 +- rn/app/src/config/messages.js | 32 ++- rn/app/src/config/scannerConfig.js | 25 ++ rn/app/src/database/SQLiteDatabase.js | 10 - rn/app/src/hooks/useAppMode.js | 6 +- rn/app/src/hooks/useAppNavigation.js | 18 +- rn/app/src/hooks/useAuth.js | 12 + rn/app/src/hooks/usePreTripInspections.js | 131 ----------- rn/app/src/hooks/useStartupSessionCheck.js | 89 ++++++++ rn/app/src/screens/AuthScreen.js | 187 ++++++++++----- rn/app/src/screens/MainScreen.js | 214 ++++-------------- rn/app/src/screens/SettingsScreen.js | 55 ++++- rn/app/src/styles/common/Backdrop.styles.js | 34 --- .../inspections/InspectionItem.styles.js | 45 ---- .../styles/scanner/BarcodeScanner.styles.js | 48 ++++ .../styles/scanner/ScanResultModal.styles.js | 106 +++++++++ .../src/styles/scanner/ScannerArea.styles.js | 48 ++++ .../ScannerPlaceholder.styles.js} | 30 +-- .../src/styles/screens/MainScreen.styles.js | 14 -- rn/app/src/utils/appInfo.js | 12 +- rn/app/src/utils/authFormStore.js | 57 +++++ rn/app/src/utils/loginFormUtils.js | 7 +- 40 files changed, 1116 insertions(+), 753 deletions(-) delete mode 100644 rn/app/src/components/common/Backdrop.js delete mode 100644 rn/app/src/components/inspections/InspectionItem.js delete mode 100644 rn/app/src/components/inspections/InspectionList.js delete mode 100644 rn/app/src/components/layout/AppPreTripInspectionsProvider.js create mode 100644 rn/app/src/components/scanner/BarcodeScanner.js create mode 100644 rn/app/src/components/scanner/BarcodeScannerNative.js create mode 100644 rn/app/src/components/scanner/BarcodeScannerWeb.js create mode 100644 rn/app/src/components/scanner/ScanResultModal.js create mode 100644 rn/app/src/components/scanner/ScannerArea.js create mode 100644 rn/app/src/components/scanner/ScannerPlaceholder.js create mode 100644 rn/app/src/config/scannerConfig.js delete mode 100644 rn/app/src/hooks/usePreTripInspections.js create mode 100644 rn/app/src/hooks/useStartupSessionCheck.js delete mode 100644 rn/app/src/styles/common/Backdrop.styles.js delete mode 100644 rn/app/src/styles/inspections/InspectionItem.styles.js create mode 100644 rn/app/src/styles/scanner/BarcodeScanner.styles.js create mode 100644 rn/app/src/styles/scanner/ScanResultModal.styles.js create mode 100644 rn/app/src/styles/scanner/ScannerArea.styles.js rename rn/app/src/styles/{inspections/InspectionList.styles.js => scanner/ScannerPlaceholder.styles.js} (55%) create mode 100644 rn/app/src/utils/authFormStore.js diff --git a/rn/app/android/app/src/main/AndroidManifest.xml b/rn/app/android/app/src/main/AndroidManifest.xml index fb78f39..a971b7c 100644 --- a/rn/app/android/app/src/main/AndroidManifest.xml +++ b/rn/app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + NSAllowsLocalNetworking + NSCameraUsageDescription + Приложению нужен доступ к камере для сканирования штрихкодов и QR-кодов. NSLocationWhenInUseUsageDescription UILaunchStoryboardName diff --git a/rn/app/package.json b/rn/app/package.json index 20bf981..1208b09 100644 --- a/rn/app/package.json +++ b/rn/app/package.json @@ -1,7 +1,7 @@ { - "name": "app", - "version": "0.0.1", - "private": true, + "name": "parus_pre_trip_inspections", + "version": "1.0.0", + "description": "Parus 8 and Pre-Trip Inspections application", "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", @@ -14,7 +14,8 @@ "react": "^19.2.0", "react-native": "^0.83.1", "react-native-quick-sqlite": "^8.2.7", - "react-native-safe-area-context": "^5.5.2" + "react-native-safe-area-context": "^5.5.2", + "react-native-vision-camera": "^4.7.3" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/rn/app/src/components/common/Backdrop.js b/rn/app/src/components/common/Backdrop.js deleted file mode 100644 index a02262f..0000000 --- a/rn/app/src/components/common/Backdrop.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Компонент заднего фона (оверлей) -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const React = require('react'); //React -const { Pressable } = require('react-native'); //Базовые компоненты -const styles = require('../../styles/common/Backdrop.styles'); //Стили заднего фона - -//----------- -//Тело модуля -//----------- - -//Задний фон для модальных окон и меню -function Backdrop({ visible, onPress, style, children }) { - const handlePress = React.useCallback(() => { - if (typeof onPress === 'function') { - onPress(); - } - }, [onPress]); - - if (!visible) { - return null; - } - - return ( - - {children} - - ); -} - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = Backdrop; diff --git a/rn/app/src/components/inspections/InspectionItem.js b/rn/app/src/components/inspections/InspectionItem.js deleted file mode 100644 index eaa218d..0000000 --- a/rn/app/src/components/inspections/InspectionItem.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Элемент списка предрейсовых осмотров -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const React = require('react'); //React -const { View } = require('react-native'); //Базовые компоненты -const AppText = require('../common/AppText'); //Общий текстовый компонент -const styles = require('../../styles/inspections/InspectionItem.styles'); //Стили элемента - -//----------- -//Тело модуля -//----------- - -//Элемент списка осмотров -function InspectionItem({ item }) { - return ( - - {item.title} - - Статус: {item.status} - Создан: {item.createdAt} - - - ); -} - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = InspectionItem; diff --git a/rn/app/src/components/inspections/InspectionList.js b/rn/app/src/components/inspections/InspectionList.js deleted file mode 100644 index e953138..0000000 --- a/rn/app/src/components/inspections/InspectionList.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Список предрейсовых осмотров -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const React = require('react'); //React -const { ActivityIndicator, FlatList, RefreshControl, View } = require('react-native'); //Базовые компоненты списка -const AppText = require('../common/AppText'); //Общий текстовый компонент -const AppButton = require('../common/AppButton'); //Общая кнопка -const InspectionItem = require('./InspectionItem'); //Элемент списка -const styles = require('../../styles/inspections/InspectionList.styles'); //Стили списка - -//----------- -//Тело модуля -//----------- - -//Список осмотров -function InspectionList({ inspections, isLoading, error, onRefresh }) { - const hasData = Array.isArray(inspections) && inspections.length > 0; - - const renderItem = React.useCallback(({ item }) => , []); - - const keyExtractor = React.useCallback(item => item.id, []); - - const handleRefresh = React.useCallback(() => { - if (typeof onRefresh === 'function') onRefresh(); - }, [onRefresh]); - - if (!hasData && isLoading) { - return ( - - - Загружаем данные... - - ); - } - - if (!hasData && !isLoading) { - return ( - - Нет данных предрейсовых осмотров - {error ? {error} : null} - - - - - ); - } - - return ( - } - /> - ); -} - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = InspectionList; diff --git a/rn/app/src/components/layout/AppAuthProvider.js b/rn/app/src/components/layout/AppAuthProvider.js index 47eb557..98b6655 100644 --- a/rn/app/src/components/layout/AppAuthProvider.js +++ b/rn/app/src/components/layout/AppAuthProvider.js @@ -21,35 +21,25 @@ const AppAuthContext = React.createContext(null); function AppAuthProvider({ children }) { const auth = useAuth(); - //Состояние для хранения данных формы авторизации - const [authFormData, setAuthFormData] = React.useState({ - serverUrl: '', - username: '', - password: '', - savePassword: false - }); + //Флаг: выполняется проверка соединения при старте (блокирует UI главного экрана) + const [isStartupSessionCheckInProgress, setStartupSessionCheckInProgress] = React.useState(false); - //Флаг проверки сессии - const [sessionChecked, setSessionChecked] = React.useState(false); + //Адрес сервера, сохранённый в настройках при текущем визите (для подстановки при возврате на экран логина) + const lastSavedServerUrlFromSettingsRef = React.useRef(null); - //Обновление данных формы авторизации - const updateAuthFormData = React.useCallback(data => { - setAuthFormData(prev => ({ ...prev, ...data })); + //Очистка данных формы авторизации (после успешного входа; актуальные данные хранятся в authFormStore и сбрасываются на экране) + const clearAuthFormData = React.useCallback(() => {}, []); + + //Отметка о сохранении адреса сервера в настройках (вызывается с экрана настроек при успешном сохранении) + const setLastSavedServerUrlFromSettings = React.useCallback(url => { + lastSavedServerUrlFromSettingsRef.current = url != null ? String(url).trim() : null; }, []); - //Очистка данных формы авторизации - const clearAuthFormData = React.useCallback(() => { - setAuthFormData({ - serverUrl: '', - username: '', - password: '', - savePassword: false - }); - }, []); - - //Отметка что сессия была проверена - const markSessionChecked = React.useCallback(() => { - setSessionChecked(true); + //Получить и сбросить адрес сервера, сохранённый в настройках (для восстановления при возврате на экран логина) + const getAndClearLastSavedServerUrlFromSettings = React.useCallback(() => { + const url = lastSavedServerUrlFromSettingsRef.current; + lastSavedServerUrlFromSettingsRef.current = null; + return url; }, []); //Мемоизация значения контекста с перечислением отдельных свойств @@ -58,6 +48,7 @@ function AppAuthProvider({ children }) { session: auth.session, isAuthenticated: auth.isAuthenticated, isLoading: auth.isLoading, + isLogoutInProgress: auth.isLogoutInProgress, isInitialized: auth.isInitialized, error: auth.error, login: auth.login, @@ -71,16 +62,17 @@ function AppAuthProvider({ children }) { clearAuthSession: auth.clearAuthSession, getAndClearSessionRestoredFromStorage: auth.getAndClearSessionRestoredFromStorage, AUTH_SETTINGS_KEYS: auth.AUTH_SETTINGS_KEYS, - authFormData, - updateAuthFormData, + isStartupSessionCheckInProgress, + setStartupSessionCheckInProgress, clearAuthFormData, - sessionChecked, - markSessionChecked + setLastSavedServerUrlFromSettings, + getAndClearLastSavedServerUrlFromSettings }), [ auth.session, auth.isAuthenticated, auth.isLoading, + auth.isLogoutInProgress, auth.isInitialized, auth.error, auth.login, @@ -94,11 +86,11 @@ function AppAuthProvider({ children }) { auth.clearAuthSession, auth.getAndClearSessionRestoredFromStorage, auth.AUTH_SETTINGS_KEYS, - authFormData, - updateAuthFormData, + isStartupSessionCheckInProgress, + setStartupSessionCheckInProgress, clearAuthFormData, - sessionChecked, - markSessionChecked + setLastSavedServerUrlFromSettings, + getAndClearLastSavedServerUrlFromSettings ] ); diff --git a/rn/app/src/components/layout/AppHeader.js b/rn/app/src/components/layout/AppHeader.js index adfb27d..0302cba 100644 --- a/rn/app/src/components/layout/AppHeader.js +++ b/rn/app/src/components/layout/AppHeader.js @@ -11,7 +11,6 @@ const React = require('react'); //React const { View, Pressable } = require('react-native'); //Базовые компоненты const AppText = require('../common/AppText'); //Общий текстовый компонент const AppLogo = require('../common/AppLogo'); //Логотип приложения -const { useAppModeContext } = require('./AppModeProvider'); //Контекст режима работы const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации const { SCREEN_TITLE_SETTINGS } = require('../../config/messages'); //Заголовки экранов const styles = require('../../styles/layout/AppHeader.styles'); //Стили заголовка @@ -50,7 +49,6 @@ function BackButton({ onPress }) { //Заголовок приложения function AppHeader({ title, subtitle, showMenuButton = true, onMenuPress, showBackButton = false, onBackPress }) { - const { mode } = useAppModeContext(); const { currentScreen, SCREENS } = useAppNavigationContext(); //Получение заголовка экрана @@ -73,13 +71,13 @@ function AppHeader({ title, subtitle, showMenuButton = true, onMenuPress, showBa switch (currentScreen) { case SCREENS.MAIN: - return mode === 'NOT_CONNECTED' ? 'Требуется настройка сервера' : 'Демонстрационный экран'; + return 'Главный экран'; case SCREENS.SETTINGS: return 'Конфигурация приложения'; default: return ''; } - }, [subtitle, currentScreen, mode, SCREENS.MAIN, SCREENS.SETTINGS]); + }, [subtitle, currentScreen, SCREENS.MAIN, SCREENS.SETTINGS]); //Стиль кнопки меню при нажатии const getMenuButtonPressableStyle = React.useMemo(() => { diff --git a/rn/app/src/components/layout/AppPreTripInspectionsProvider.js b/rn/app/src/components/layout/AppPreTripInspectionsProvider.js deleted file mode 100644 index d427744..0000000 --- a/rn/app/src/components/layout/AppPreTripInspectionsProvider.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Провайдер контекста предметной области "Предрейсовые осмотры" -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const React = require('react'); //React и хуки -const usePreTripInspections = require('../../hooks/usePreTripInspections'); //Хук предметной области - -//----------- -//Тело модуля -//----------- - -//Контекст предрейсовых осмотров -const AppPreTripInspectionsContext = React.createContext(null); - -//Провайдер предрейсовых осмотров -function AppPreTripInspectionsProvider({ children }) { - const inspectionsApi = usePreTripInspections(); - - //Мемоизация значения контекста с перечислением отдельных свойств - const value = React.useMemo( - () => ({ - inspections: inspectionsApi.inspections, - loadStatus: inspectionsApi.loadStatus, - error: inspectionsApi.error, - isDbReady: inspectionsApi.isDbReady, - refreshInspections: inspectionsApi.refreshInspections, - upsertInspection: inspectionsApi.upsertInspection - }), - [ - inspectionsApi.inspections, - inspectionsApi.loadStatus, - inspectionsApi.error, - inspectionsApi.isDbReady, - inspectionsApi.refreshInspections, - inspectionsApi.upsertInspection - ] - ); - - return {children}; -} - -//Хук доступа к контексту предрейсовых осмотров -function useAppPreTripInspectionsContext() { - const ctx = React.useContext(AppPreTripInspectionsContext); - if (!ctx) { - throw new Error('useAppPreTripInspectionsContext должен использоваться внутри AppPreTripInspectionsProvider'); - } - return ctx; -} - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = { - AppPreTripInspectionsProvider, - useAppPreTripInspectionsContext -}; diff --git a/rn/app/src/components/layout/AppRoot.js b/rn/app/src/components/layout/AppRoot.js index b72bd35..526cc30 100644 --- a/rn/app/src/components/layout/AppRoot.js +++ b/rn/app/src/components/layout/AppRoot.js @@ -15,6 +15,7 @@ const styles = require('../../styles/layout/AppRoot.styles'); //Стили ко const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации const { useAppLocalDbContext } = require('./AppLocalDbProvider'); //Контекст локальной БД +const useStartupSessionCheck = require('../../hooks/useStartupSessionCheck'); //Проверка сессии при старте //----------- //Тело модуля @@ -29,6 +30,9 @@ function AppRoot() { const { isAuthenticated, isInitialized } = useAppAuthContext(); const { isDbReady } = useAppLocalDbContext(); + //Проверка сессии при старте и установка режима онлайн/оффлайн + useStartupSessionCheck(); + //Флаг для предотвращения повторной установки начального экрана const initialScreenSetRef = React.useRef(false); diff --git a/rn/app/src/components/layout/AppShell.js b/rn/app/src/components/layout/AppShell.js index 920fdad..b364a62 100644 --- a/rn/app/src/components/layout/AppShell.js +++ b/rn/app/src/components/layout/AppShell.js @@ -10,10 +10,13 @@ const React = require('react'); //React const { StatusBar, Platform } = require('react-native'); //Базовые компоненты const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации +const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации const MainScreen = require('../../screens/MainScreen'); //Главный экран const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации const AdaptiveView = require('../common/AdaptiveView'); //Адаптивный контейнер +const LoadingOverlay = require('../common/LoadingOverlay'); //Оверлей загрузки +const { STARTUP_CHECK_CONNECTION_MESSAGE, LOGOUT_IN_PROGRESS_MESSAGE } = require('../../config/messages'); //Сообщения const styles = require('../../styles/layout/AppShell.styles'); //Стили оболочки //----------- @@ -23,6 +26,7 @@ const styles = require('../../styles/layout/AppShell.styles'); //Стили об //Оболочка приложения function AppShell({ isDarkMode }) { const { currentScreen, SCREENS } = useAppNavigationContext(); + const { isStartupSessionCheckInProgress, isLogoutInProgress } = useAppAuthContext(); const renderScreen = React.useCallback(() => { switch (currentScreen) { @@ -41,10 +45,15 @@ function AppShell({ isDarkMode }) { const statusBarStyle = isDarkMode ? 'light-content' : 'dark-content'; const statusBarBackground = isDarkMode ? '#0F172A' : '#F8FAFC'; + const showStartupOverlay = currentScreen === SCREENS.MAIN && isStartupSessionCheckInProgress; + const showLogoutOverlay = currentScreen === SCREENS.MAIN && isLogoutInProgress; + return ( <> {renderScreen()} + + ); } diff --git a/rn/app/src/components/scanner/BarcodeScanner.js b/rn/app/src/components/scanner/BarcodeScanner.js new file mode 100644 index 0000000..d2cac4a --- /dev/null +++ b/rn/app/src/components/scanner/BarcodeScanner.js @@ -0,0 +1,32 @@ +/* + Предрейсовые осмотры - мобильное приложение + Обёртка сканера +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React +const { Platform } = require('react-native'); //Платформа +const BarcodeScannerWeb = require('./BarcodeScannerWeb'); //Заглушка для веб + +//----------- +//Тело модуля +//----------- + +//Компонент сканера +function BarcodeScanner(props) { + if (Platform.OS === 'web') { + return ; + } + + const BarcodeScannerNative = require('./BarcodeScannerNative'); + return ; +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = BarcodeScanner; diff --git a/rn/app/src/components/scanner/BarcodeScannerNative.js b/rn/app/src/components/scanner/BarcodeScannerNative.js new file mode 100644 index 0000000..b0a15e1 --- /dev/null +++ b/rn/app/src/components/scanner/BarcodeScannerNative.js @@ -0,0 +1,104 @@ +/* + Предрейсовые осмотры - мобильное приложение + Сканер штрихкодов и QR +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React и хуки +const { View } = require('react-native'); //Базовые компоненты +const { Camera, useCameraDevice, useCodeScanner, useCameraPermission } = require('react-native-vision-camera'); //Камера и сканер кодов +const AppText = require('../common/AppText'); //Общий текст +const { DEFAULT_CODE_TYPES } = require('../../config/scannerConfig'); //Конфиг сканера +const styles = require('../../styles/scanner/BarcodeScanner.styles'); //Стили сканера + +//----------- +//Тело модуля +//----------- + +//Текст при отсутствии разрешения на камеру +const PERMISSION_DENIED_MESSAGE = 'Для сканирования разрешите доступ к камере в настройках приложения'; + +//Текст при отсутствии устройства камеры +const NO_CAMERA_MESSAGE = 'Камера недоступна'; + +//Обработка отсканированных кодов +function createCodeScannedHandler(onScan, lastScannedRef) { + return function onCodeScanned(codes) { + if (!codes || codes.length === 0 || typeof onScan !== 'function') { + return; + } + const code = codes[0]; + const value = code.value; + if (value == null || value === '') { + return; + } + if (lastScannedRef.current === value) { + return; + } + lastScannedRef.current = value; + onScan({ type: code.type || 'unknown', value }); + }; +} + +//Сканер штрихкодов и QR (нативная камера) +function BarcodeScannerNative({ onScan, isActive = true }) { + const device = useCameraDevice('back'); + const { hasPermission, requestPermission } = useCameraPermission(); + const lastScannedRef = React.useRef(null); + + const onCodeScannedCallback = React.useCallback( + codes => { + createCodeScannedHandler(onScan, lastScannedRef)(codes); + }, + [onScan] + ); + + const codeScanner = useCodeScanner({ + codeTypes: DEFAULT_CODE_TYPES, + onCodeScanned: onCodeScannedCallback + }); + + //Запрос разрешения при монтировании + React.useEffect(() => { + if (hasPermission === false && typeof requestPermission === 'function') { + requestPermission(); + } + }, [hasPermission, requestPermission]); + + //Нет устройства камеры + if (device == null) { + return ( + + + {NO_CAMERA_MESSAGE} + + + ); + } + + //Нет разрешения на камеру + if (!hasPermission) { + return ( + + + {PERMISSION_DENIED_MESSAGE} + + + ); + } + + return ( + + + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = BarcodeScannerNative; diff --git a/rn/app/src/components/scanner/BarcodeScannerWeb.js b/rn/app/src/components/scanner/BarcodeScannerWeb.js new file mode 100644 index 0000000..144ac0d --- /dev/null +++ b/rn/app/src/components/scanner/BarcodeScannerWeb.js @@ -0,0 +1,37 @@ +/* + Предрейсовые осмотры - мобильное приложение + Заглушка сканера для веб-платформы (камера недоступна) +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React +const { View } = require('react-native'); //Базовые компоненты +const AppText = require('../common/AppText'); //Общий текст +const styles = require('../../styles/scanner/BarcodeScanner.styles'); //Стили сканера + +//----------- +//Тело модуля +//----------- + +//Текст заглушки для веб +const WEB_SCANNER_UNAVAILABLE = 'Сканирование доступно только в мобильном приложении'; + +//Компонент-заглушка для веб: камера не поддерживается +function BarcodeScannerWeb() { + return ( + + + {WEB_SCANNER_UNAVAILABLE} + + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = BarcodeScannerWeb; diff --git a/rn/app/src/components/scanner/ScanResultModal.js b/rn/app/src/components/scanner/ScanResultModal.js new file mode 100644 index 0000000..c935344 --- /dev/null +++ b/rn/app/src/components/scanner/ScanResultModal.js @@ -0,0 +1,84 @@ +/* + Предрейсовые осмотры - мобильное приложение + Модальное окно с результатом сканирования штрихкода/QR +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React +const { Modal, View, Pressable } = require('react-native'); //Базовые компоненты +const AppText = require('../common/AppText'); //Общий текстовый компонент +const AppButton = require('../common/AppButton'); //Кнопка +const { SCAN_RESULT_MODAL_TITLE, SCAN_RESULT_CLOSE_BUTTON } = require('../../config/messages'); //Сообщения +const styles = require('../../styles/scanner/ScanResultModal.styles'); //Стили модального окна + +//----------- +//Тело модуля +//----------- + +//Форматирование типа кода для отображения (например qr -> QR-код) +function formatCodeType(type) { + if (!type || type === 'unknown') { + return 'Неизвестный тип'; + } + const upper = type.toUpperCase().replace(/-/g, ' '); + if (type === 'qr') { + return 'QR-код'; + } + return upper; +} + +//Обработчик закрытия модального окна +function handleClose(onRequestClose) { + if (typeof onRequestClose === 'function') { + onRequestClose(); + } +} + +//Модальное окно результата сканирования +function ScanResultModal({ visible, codeType, value, onRequestClose }) { + const handleClosePress = React.useCallback(() => { + handleClose(onRequestClose); + }, [onRequestClose]); + + const displayType = formatCodeType(codeType); + const displayValue = value != null && value !== '' ? String(value) : '—'; + + return ( + + + + + + {SCAN_RESULT_MODAL_TITLE} + + + × + + + + + Тип: {displayType} + + + + {displayValue} + + + + + + + + + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = ScanResultModal; diff --git a/rn/app/src/components/scanner/ScannerArea.js b/rn/app/src/components/scanner/ScannerArea.js new file mode 100644 index 0000000..cb5901a --- /dev/null +++ b/rn/app/src/components/scanner/ScannerArea.js @@ -0,0 +1,89 @@ +/* + Предрейсовые осмотры - мобильное приложение + Область сканера на главном экране: всегда активный сканер или заглушка с кнопкой +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React +const { View, Modal, Pressable } = require('react-native'); //Базовые компоненты +const BarcodeScanner = require('./BarcodeScanner'); //Сканер +const ScannerPlaceholder = require('./ScannerPlaceholder'); //Заглушка с кнопкой +const AppText = require('../common/AppText'); //Текст +const styles = require('../../styles/scanner/ScannerArea.styles'); //Стили области + +//----------- +//Тело модуля +//----------- + +//Обработчик успешного сканирования: передаём результат наружу и при необходимости закрываем модал +function handleScanResult(result, onScanResult, closeModal) { + if (typeof onScanResult === 'function') { + onScanResult(result); + } + if (typeof closeModal === 'function') { + closeModal(); + } +} + +//Область сканера: при включённой настройке — активный сканер (если открыт), иначе заглушка и модальный сканер по кнопке +function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResult }) { + const [scannerModalVisible, setScannerModalVisible] = React.useState(false); + + const handleScanFromArea = React.useCallback( + result => { + handleScanResult(result, onScanResult, null); + }, + [onScanResult] + ); + + const handleScanFromModal = React.useCallback( + result => { + handleScanResult(result, onScanResult, () => setScannerModalVisible(false)); + }, + [onScanResult] + ); + + const handleOpenScanner = React.useCallback(() => { + setScannerModalVisible(true); + }, []); + + const handleCloseScannerModal = React.useCallback(() => { + setScannerModalVisible(false); + }, []); + + const renderScannerContent = () => { + if (alwaysShowScanner && scannerOpen) { + return ; + } + return ; + }; + + return ( + + {renderScannerContent()} + + + + + + Закрыть + + + + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = ScannerArea; diff --git a/rn/app/src/components/scanner/ScannerPlaceholder.js b/rn/app/src/components/scanner/ScannerPlaceholder.js new file mode 100644 index 0000000..01b3dc9 --- /dev/null +++ b/rn/app/src/components/scanner/ScannerPlaceholder.js @@ -0,0 +1,39 @@ +/* + Предрейсовые осмотры - мобильное приложение + Заглушка области сканера с кнопкой «Сканировать» +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React +const { View } = require('react-native'); //Базовые компоненты +const AppButton = require('../common/AppButton'); //Кнопка +const { SCAN_BUTTON_TITLE } = require('../../config/messages'); //Сообщения +const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Стили заглушки + +//----------- +//Тело модуля +//----------- + +//Заглушка: затемнённая область и кнопка открытия сканера +function ScannerPlaceholder({ onScanPress }) { + const handlePress = React.useCallback(() => { + if (typeof onScanPress === 'function') { + onScanPress(); + } + }, [onScanPress]); + + return ( + + + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = ScannerPlaceholder; diff --git a/rn/app/src/config/authConfig.js b/rn/app/src/config/authConfig.js index b786584..84c3ae2 100644 --- a/rn/app/src/config/authConfig.js +++ b/rn/app/src/config/authConfig.js @@ -10,13 +10,15 @@ //Ключи настроек авторизации const AUTH_SETTINGS_KEYS = { SERVER_URL: 'app_server_url', + LAST_CONNECTED_SERVER_URL: 'auth_last_connected_server_url', HIDE_SERVER_URL: 'auth_hide_server_url', IDLE_TIMEOUT: 'auth_idle_timeout', DEVICE_ID: 'auth_device_id', DEVICE_SECRET_KEY: 'auth_device_secret_key', SAVED_LOGIN: 'auth_saved_login', SAVED_PASSWORD: 'auth_saved_password', - SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled' + SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled', + ALWAYS_SHOW_SCANNER: 'main_always_show_scanner' }; //Значение времени простоя по умолчанию (минуты) diff --git a/rn/app/src/config/messages.js b/rn/app/src/config/messages.js index 0b599be..49271cf 100644 --- a/rn/app/src/config/messages.js +++ b/rn/app/src/config/messages.js @@ -13,6 +13,9 @@ const CONNECTION_LOST_MESSAGE = 'Нет связи с сервером. Прил //Заголовок сообщения при переходе в режим офлайн const OFFLINE_MODE_TITLE = 'Режим офлайн'; +//Сообщение при проверке соединения при старте приложения +const STARTUP_CHECK_CONNECTION_MESSAGE = 'Проверка соединения...'; + //Заголовок диалога/экрана «Информация о приложении» const APP_ABOUT_TITLE = 'Информация о приложении'; @@ -32,16 +35,27 @@ const MENU_ITEM_LOGOUT = 'Выход'; const AUTH_SCREEN_TITLE = 'Вход в приложение'; const AUTH_BUTTON_LOGIN = 'Войти'; const AUTH_BUTTON_LOADING = 'Вход...'; +const LOGOUT_IN_PROGRESS_MESSAGE = 'Выход...'; + +//Диалог подтверждения при смене сервера (относительно последнего успешного подключения) +const AUTH_SERVER_CHANGE_CONFIRM_TITLE = 'Подтверждение входа'; +const AUTH_SERVER_CHANGE_CONFIRM_MESSAGE = 'Сервер подключения изменился относительно последнего входа. Локальные данные будут сброшены. Продолжить?'; +const AUTH_SERVER_CHANGE_CONFIRM_BUTTON = 'Продолжить'; +const AUTH_SERVER_CHANGE_CANCEL_BUTTON = 'Отмена'; //Сообщения об успешных действиях -const LOGIN_SUCCESS_MESSAGE = 'Вход выполнен успешно'; -const LOGOUT_SUCCESS_MESSAGE = 'Выход выполнен'; const SETTINGS_SERVER_SAVED_MESSAGE = 'Настройки сервера сохранены'; const SETTINGS_RESET_SUCCESS_MESSAGE = 'Настройки сброшены'; //Заголовок экрана настроек const SCREEN_TITLE_SETTINGS = 'Настройки'; +//Сканер на главном экране +const SCANNER_SETTING_LABEL = 'Всегда отображать сканер на главном экране'; +const SCAN_BUTTON_TITLE = 'Сканировать'; +const SCAN_RESULT_MODAL_TITLE = 'Результат сканирования'; +const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть'; + //---------------- //Интерфейс модуля //---------------- @@ -49,6 +63,7 @@ const SCREEN_TITLE_SETTINGS = 'Настройки'; module.exports = { CONNECTION_LOST_MESSAGE, OFFLINE_MODE_TITLE, + STARTUP_CHECK_CONNECTION_MESSAGE, APP_ABOUT_TITLE, SIDE_MENU_TITLE, ORGANIZATION_SELECT_DIALOG_TITLE, @@ -59,9 +74,16 @@ module.exports = { AUTH_SCREEN_TITLE, AUTH_BUTTON_LOGIN, AUTH_BUTTON_LOADING, - LOGIN_SUCCESS_MESSAGE, - LOGOUT_SUCCESS_MESSAGE, + LOGOUT_IN_PROGRESS_MESSAGE, + AUTH_SERVER_CHANGE_CONFIRM_TITLE, + AUTH_SERVER_CHANGE_CONFIRM_MESSAGE, + AUTH_SERVER_CHANGE_CONFIRM_BUTTON, + AUTH_SERVER_CHANGE_CANCEL_BUTTON, SETTINGS_SERVER_SAVED_MESSAGE, SETTINGS_RESET_SUCCESS_MESSAGE, - SCREEN_TITLE_SETTINGS + SCREEN_TITLE_SETTINGS, + SCANNER_SETTING_LABEL, + SCAN_BUTTON_TITLE, + SCAN_RESULT_MODAL_TITLE, + SCAN_RESULT_CLOSE_BUTTON }; diff --git a/rn/app/src/config/scannerConfig.js b/rn/app/src/config/scannerConfig.js new file mode 100644 index 0000000..98be1b6 --- /dev/null +++ b/rn/app/src/config/scannerConfig.js @@ -0,0 +1,25 @@ +/* + Предрейсовые осмотры - мобильное приложение + Конфигурация сканера штрихкодов и QR-кодов +*/ + +//--------- +//Константы +//--------- + +//Типы кодов для распознавания: QR и распространённые штрихкоды +const DEFAULT_CODE_TYPES = ['qr', 'code-128', 'code-39', 'ean-13', 'ean-8', 'upc-a', 'upc-e', 'pdf-417', 'aztec', 'data-matrix']; + +//Определение, должен ли отображаться сканер на главном экране +function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInProgress }) { + return Boolean(alwaysShowScanner && !hasScanResult && !isStartupCheckInProgress); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = { + DEFAULT_CODE_TYPES, + shouldShowScanner +}; diff --git a/rn/app/src/database/SQLiteDatabase.js b/rn/app/src/database/SQLiteDatabase.js index 573a499..ba31268 100644 --- a/rn/app/src/database/SQLiteDatabase.js +++ b/rn/app/src/database/SQLiteDatabase.js @@ -38,8 +38,6 @@ class SQLiteDatabase { location: 'default' }); - console.log('База данных успешно открыта'); - //Настраиваем базу данных await this.setupDatabase(); @@ -60,21 +58,14 @@ class SQLiteDatabase { try { //Выполняем SQL запросы последовательно await this.executeQuery(this.sqlQueries.CREATE_TABLE_APP_SETTINGS); - console.log('Таблица app_settings создана/проверена'); await this.executeQuery(this.sqlQueries.CREATE_TABLE_INSPECTIONS); - console.log('Таблица inspections создана/проверена'); await this.executeQuery(this.sqlQueries.CREATE_TABLE_AUTH_SESSION); - console.log('Таблица auth_session создана/проверена'); await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS); - console.log('Индекс idx_inspections_status создан/проверен'); await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_CREATED); - console.log('Индекс idx_inspections_created создан/проверен'); - - console.log('Все таблицы и индексы созданы/проверены'); } catch (error) { console.error('Ошибка настройки базы данных:', error); throw error; @@ -398,7 +389,6 @@ class SQLiteDatabase { // База данных закрывается автоматически при уничтожении объекта this.db = null; this.isInitialized = false; - console.log('База данных закрыта'); } } catch (error) { console.error('Ошибка закрытия базы данных:', error); diff --git a/rn/app/src/hooks/useAppMode.js b/rn/app/src/hooks/useAppMode.js index d38e559..21ad5cc 100644 --- a/rn/app/src/hooks/useAppMode.js +++ b/rn/app/src/hooks/useAppMode.js @@ -49,12 +49,8 @@ function useAppMode() { const savedMode = await getSetting(STORAGE_KEY); if (savedMode && Object.values(APP_MODE).includes(savedMode)) { setMode(savedMode); - } else { - const serverUrl = await getSetting('app_server_url'); - if (serverUrl) { - setMode(APP_MODE.ONLINE); - } } + //При отсутствии сохранённого режима остаётся NOT_CONNECTED } catch (error) { console.error('Ошибка загрузки режима:', error); } finally { diff --git a/rn/app/src/hooks/useAppNavigation.js b/rn/app/src/hooks/useAppNavigation.js index 33177c6..22ec09e 100644 --- a/rn/app/src/hooks/useAppNavigation.js +++ b/rn/app/src/hooks/useAppNavigation.js @@ -17,10 +17,11 @@ const { SCREENS } = require('../config/routes'); //Экраны навигаци //Хук навигации приложения const useAppNavigation = () => { //Начальный экран - AUTH (до определения статуса авторизации) + //История хранит пары { screen, params } для сохранения параметров при возврате const [navigationState, setNavigationState] = React.useState({ currentScreen: SCREENS.AUTH, screenParams: {}, - history: [SCREENS.AUTH] + history: [{ screen: SCREENS.AUTH, params: {} }] }); //Навигация на экран @@ -28,24 +29,23 @@ const useAppNavigation = () => { setNavigationState(prev => ({ currentScreen: screen, screenParams: params, - history: [...prev.history, screen] + history: [...prev.history, { screen, params }] })); }, []); - //Возврат назад + //Возврат назад (восстанавливаем экран и его параметры из истории) const goBack = React.useCallback(() => { setNavigationState(prev => { if (prev.history.length <= 1) { return prev; } - const newHistory = [...prev.history]; - newHistory.pop(); - const previousScreen = newHistory[newHistory.length - 1]; + const newHistory = prev.history.slice(0, -1); + const previous = newHistory[newHistory.length - 1]; return { - currentScreen: previousScreen, - screenParams: {}, + currentScreen: previous.screen, + screenParams: previous.params || {}, history: newHistory }; }); @@ -65,7 +65,7 @@ const useAppNavigation = () => { setNavigationState({ currentScreen: screen, screenParams: params, - history: [screen] + history: [{ screen, params }] }); }, []); diff --git a/rn/app/src/hooks/useAuth.js b/rn/app/src/hooks/useAuth.js index c9c830e..232423e 100644 --- a/rn/app/src/hooks/useAuth.js +++ b/rn/app/src/hooks/useAuth.js @@ -26,6 +26,7 @@ function useAuth() { const [session, setSession] = React.useState(null); const [isAuthenticated, setIsAuthenticated] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); + const [isLogoutInProgress, setIsLogoutInProgress] = React.useState(false); const [isInitialized, setIsInitialized] = React.useState(false); const [error, setError] = React.useState(null); @@ -286,10 +287,14 @@ function useAuth() { await setAuthSession(sessionData); await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl); + await setSetting(AUTH_SETTINGS_KEYS.LAST_CONNECTED_SERVER_URL, serverUrl); await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, user); if (savePassword) { await saveCredentials(user, password, deviceId); + } else { + await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, ''); + await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false'); } setSession(sessionData); @@ -365,6 +370,7 @@ function useAuth() { await setAuthSession(sessionData); await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl); + await setSetting(AUTH_SETTINGS_KEYS.LAST_CONNECTED_SERVER_URL, serverUrl); const loginToSave = loginCredentials?.login || user?.SCODE || user?.SNAME || ''; if (loginToSave) { await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, loginToSave); @@ -372,6 +378,9 @@ function useAuth() { if (savePassword && loginCredentials) { await saveCredentials(loginCredentials.login, loginCredentials.password, loginCredentials.deviceId); + } else { + await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, ''); + await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false'); } setSession(sessionData); @@ -399,6 +408,7 @@ function useAuth() { const { skipServerRequest = false } = options; setIsLoading(true); + setIsLogoutInProgress(true); setError(null); try { @@ -435,6 +445,7 @@ function useAuth() { return { success: false, error: errorMessage }; } finally { setIsLoading(false); + setIsLogoutInProgress(false); } }, [session, getAuthSession, executeRequest, clearAuthSession] @@ -601,6 +612,7 @@ function useAuth() { session, isAuthenticated, isLoading, + isLogoutInProgress, isInitialized, error, diff --git a/rn/app/src/hooks/usePreTripInspections.js b/rn/app/src/hooks/usePreTripInspections.js deleted file mode 100644 index 54cdb53..0000000 --- a/rn/app/src/hooks/usePreTripInspections.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Хук предметной области "Предрейсовые осмотры" - - Объединяет: - - работу с сервером приложений; - - работу с локальной базой данных; - - управление состоянием загрузки/ошибок. -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const React = require('react'); //React и хуки -const useAppServer = require('./useAppServer'); //Хук для сервера приложений -const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД -const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы -const { LOAD_STATUS_IDLE, LOAD_STATUS_LOADING, LOAD_STATUS_DONE, LOAD_STATUS_ERROR } = require('../config/loadStatus'); //Статусы загрузки - -//----------- -//Тело модуля -//----------- - -//Хук предметной области "Предрейсовые осмотры" -function usePreTripInspections() { - const { executeAction, isRespErr, getRespErrMessage, RESP_STATUS_OK } = useAppServer(); - const { inspections, loadInspections, saveInspection, isDbReady } = useAppLocalDbContext(); - const { APP_MODE, mode } = useAppModeContext(); - - const [loadStatus, setLoadStatus] = React.useState(LOAD_STATUS_IDLE); - const [error, setError] = React.useState(null); - - //Загрузка списка осмотров: - //1) Если режим OFFLINE - работаем только с локальной БД; - //2) Если режим ONLINE - пробуем получить данные с сервера приложений, - // при ошибке используем локальные данные. - const refreshInspections = React.useCallback(async () => { - //Проверяем готовность базы данных - if (!isDbReady) { - return { - inspections: [], - fromServer: false - }; - } - - setLoadStatus(LOAD_STATUS_LOADING); - setError(null); - - //Режим OFFLINE - работаем только с локальными данными - if (mode === APP_MODE.OFFLINE) { - const localInspections = await loadInspections(); - setLoadStatus(LOAD_STATUS_DONE); - return { - inspections: localInspections, - fromServer: false - }; - } - - //Режим ONLINE - пробуем получить данные с сервера приложений - const serverResponse = await executeAction({ - path: 'api/pretrip/inspections', - method: 'POST', - payload: {} - }); - - //Ошибка запроса — используем локальные данные - if (isRespErr(serverResponse) || serverResponse.status !== RESP_STATUS_OK) { - const localInspections = await loadInspections(); - setLoadStatus(localInspections.length > 0 ? LOAD_STATUS_DONE : LOAD_STATUS_ERROR); - setError(getRespErrMessage(serverResponse) || 'Не удалось получить данные с сервера приложений'); - return { - inspections: localInspections, - fromServer: false - }; - } - - //Успех - приводим полезную нагрузку к ожидаемому виду - const payload = serverResponse.payload || {}; - const resultInspections = Array.isArray(payload.inspections) ? payload.inspections : []; - - setLoadStatus(LOAD_STATUS_DONE); - - return { - inspections: resultInspections, - fromServer: true - }; - }, [APP_MODE.OFFLINE, RESP_STATUS_OK, executeAction, getRespErrMessage, isDbReady, isRespErr, loadInspections, mode]); - - //Создание или обновление осмотра - const upsertInspection = React.useCallback( - async inspection => { - const safeInspection = { - id: inspection.id || `local-${Date.now()}`, - title: inspection.title || 'Новый осмотр', - status: inspection.status || 'DRAFT', - createdAt: inspection.createdAt || new Date().toISOString() - }; - - //Сохраняем в локальной БД (всегда, независимо от режима) - await saveInspection(safeInspection); - - //Если приложение в режиме ONLINE - отправляем данные на сервер приложений - if (mode === APP_MODE.ONLINE) { - await executeAction({ - path: 'api/pretrip/inspections/save', - method: 'POST', - payload: { inspection: safeInspection } - }); - } - - return safeInspection; - }, - [APP_MODE.ONLINE, executeAction, saveInspection, mode] - ); - - return { - inspections, - loadStatus, - error, - isDbReady, - refreshInspections, - upsertInspection - }; -} - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = usePreTripInspections; diff --git a/rn/app/src/hooks/useStartupSessionCheck.js b/rn/app/src/hooks/useStartupSessionCheck.js new file mode 100644 index 0000000..a0ab4ef --- /dev/null +++ b/rn/app/src/hooks/useStartupSessionCheck.js @@ -0,0 +1,89 @@ +/* + Предрейсовые осмотры - мобильное приложение + Проверка сессии при старте приложения и установка режима онлайн/оффлайн +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const React = require('react'); //React и хуки +const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД +const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации +const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы +const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений +const { CONNECTION_LOST_MESSAGE, OFFLINE_MODE_TITLE } = require('../config/messages'); //Сообщения + +//----------- +//Тело модуля +//----------- + +//Проверка сессии при входе в приложение +function useStartupSessionCheck() { + const { isDbReady } = useAppLocalDbContext(); + const { isInitialized, isAuthenticated, checkSession, getAndClearSessionRestoredFromStorage, setStartupSessionCheckInProgress } = + useAppAuthContext(); + const { setOnline, setOffline } = useAppModeContext(); + const { showInfo } = useAppMessagingContext(); + + //Флаг: проверка соединения уже выполнена в этой сессии приложения + const checkDoneRef = React.useRef(false); + + React.useEffect(() => { + if (!isDbReady || !isInitialized || !isAuthenticated) { + return; + } + + if (checkDoneRef.current) { + return; + } + + const restored = getAndClearSessionRestoredFromStorage(); + if (!restored) { + return; + } + + checkDoneRef.current = true; + setStartupSessionCheckInProgress(true); + let cancelled = false; + + checkSession() + .then(result => { + if (cancelled || !result || !result.success) { + return; + } + if (result.isOffline) { + setOffline(); + showInfo(CONNECTION_LOST_MESSAGE, { title: OFFLINE_MODE_TITLE }); + } else { + setOnline(); + } + }) + .finally(() => { + if (!cancelled) { + setStartupSessionCheckInProgress(false); + } + }); + + return () => { + cancelled = true; + setStartupSessionCheckInProgress(false); + }; + }, [ + isDbReady, + isInitialized, + isAuthenticated, + checkSession, + getAndClearSessionRestoredFromStorage, + setStartupSessionCheckInProgress, + setOnline, + setOffline, + showInfo + ]); +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = useStartupSessionCheck; diff --git a/rn/app/src/screens/AuthScreen.js b/rn/app/src/screens/AuthScreen.js index c75d5a0..639d219 100644 --- a/rn/app/src/screens/AuthScreen.js +++ b/rn/app/src/screens/AuthScreen.js @@ -29,6 +29,7 @@ const { getAppInfo } = require('../utils/appInfo'); //Информация о п const { isServerUrlFieldVisible } = require('../utils/loginFormUtils'); //Утилиты формы входа const { normalizeServerUrl, validateServerUrl } = require('../utils/validation'); //Валидация const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации +const { getAuthFormStore, setAuthFormStore, clearAuthFormStore } = require('../utils/authFormStore'); //Хранилище формы входа const { APP_ABOUT_TITLE, SIDE_MENU_TITLE, @@ -38,7 +39,10 @@ const { AUTH_SCREEN_TITLE, AUTH_BUTTON_LOGIN, AUTH_BUTTON_LOADING, - LOGIN_SUCCESS_MESSAGE + AUTH_SERVER_CHANGE_CONFIRM_TITLE, + AUTH_SERVER_CHANGE_CONFIRM_MESSAGE, + AUTH_SERVER_CHANGE_CONFIRM_BUTTON, + AUTH_SERVER_CHANGE_CANCEL_BUTTON } = require('../config/messages'); //Сообщения const styles = require('../styles/screens/AuthScreen.styles'); //Стили экрана @@ -48,9 +52,9 @@ const styles = require('../styles/screens/AuthScreen.styles'); //Стили эк //Экран аутентификации function AuthScreen() { - const { showError, showSuccess, showInfo } = useAppMessagingContext(); + const { showError, showInfo } = useAppMessagingContext(); const { APP_MODE, mode, setOnline } = useAppModeContext(); - const { navigate, goBack, canGoBack, reset, screenParams, SCREENS } = useAppNavigationContext(); + const { navigate, goBack, canGoBack, reset, screenParams, currentScreen, SCREENS } = useAppNavigationContext(); const { getSetting, isDbReady, clearInspections } = useAppLocalDbContext(); const { session, @@ -60,23 +64,26 @@ function AuthScreen() { getSavedCredentials, getAuthSession, clearAuthSession, - authFormData, - updateAuthFormData, - clearAuthFormData + clearAuthFormData, + getAndClearLastSavedServerUrlFromSettings } = useAppAuthContext(); + //Данные формы при монтировании — из хранилища (восстанавливаются при возврате с других экранов) + const initialForm = getAuthFormStore(); + //Состояние меню const [menuVisible, setMenuVisible] = React.useState(false); //Состояние формы - const [serverUrl, setServerUrl] = React.useState(authFormData.serverUrl); - const [username, setUsername] = React.useState(authFormData.username); - const [password, setPassword] = React.useState(authFormData.password); - const [savePassword, setSavePassword] = React.useState(authFormData.savePassword); - const [showPassword, setShowPassword] = React.useState(false); + const [serverUrl, setServerUrl] = React.useState(initialForm.serverUrl || ''); + const [username, setUsername] = React.useState(initialForm.username || ''); + const [password, setPassword] = React.useState(initialForm.password || ''); + const [savePassword, setSavePassword] = React.useState(!!initialForm.savePassword); + const [showPassword, setShowPassword] = React.useState(!!initialForm.showPassword); //Состояние отображения const [hideServerUrl, setHideServerUrl] = React.useState(false); + const [savedServerUrlFromSettings, setSavedServerUrlFromSettings] = React.useState(''); const [isSettingsLoaded, setIsSettingsLoaded] = React.useState(false); //Флаг для предотвращения повторной загрузки @@ -94,31 +101,80 @@ function AuthScreen() { const isFromMenu = screenParams?.fromMenu === true; const fromMenuKey = screenParams?.fromMenuKey; - //Ref для хранения актуальных значений формы (для синхронизации при размонтировании) - const formDataRef = React.useRef({ serverUrl, username, password, savePassword }); + //Ref актуальных значений формы (для записи в store при размонтировании) + const formDataRef = React.useRef({ + serverUrl, + username, + password, + savePassword, + showPassword + }); //Ref полей ввода для перехода фокуса по Enter const serverInputRef = React.useRef(null); const loginInputRef = React.useRef(null); const passwordInputRef = React.useRef(null); - //Обновление ref при изменении значений формы + //Актуализация ref и store при изменении полей; при размонтировании — сохранение в store React.useEffect(() => { - formDataRef.current = { serverUrl, username, password, savePassword }; - }, [serverUrl, username, password, savePassword]); + const next = { serverUrl, username, password, savePassword, showPassword }; + formDataRef.current = next; + setAuthFormStore(next); + return () => setAuthFormStore(formDataRef.current); + }, [serverUrl, username, password, savePassword, showPassword]); - //Синхронизация данных формы с контекстом при размонтировании компонента - React.useEffect(() => { - return () => { - updateAuthFormData(formDataRef.current); + //Обработчики полей формы: сразу пишем в store и ref, затем setState + const handleServerUrlChange = React.useCallback(value => { + formDataRef.current.serverUrl = value; + setAuthFormStore({ serverUrl: value }); + setServerUrl(value); + }, []); + const handleUsernameChange = React.useCallback(value => { + formDataRef.current.username = value; + setAuthFormStore({ username: value }); + setUsername(value); + }, []); + const handlePasswordChange = React.useCallback(value => { + formDataRef.current.password = value; + setAuthFormStore({ password: value }); + setPassword(value); + }, []); + + //Восстановление формы при возврате на экран логина + React.useLayoutEffect(() => { + if (!canGoBack) { + return; + } + const storedForm = getAuthFormStore(); + const savedServerUrl = getAndClearLastSavedServerUrlFromSettings(); + const serverUrlVal = savedServerUrl || (storedForm.serverUrl != null ? String(storedForm.serverUrl) : ''); + const usernameVal = storedForm.username != null ? String(storedForm.username) : ''; + const passwordVal = storedForm.password != null ? String(storedForm.password) : ''; + const savePasswordVal = Boolean(storedForm.savePassword); + const showPasswordVal = Boolean(storedForm.showPassword); + setServerUrl(serverUrlVal); + setUsername(usernameVal); + setPassword(passwordVal); + setSavePassword(savePasswordVal); + setShowPassword(showPasswordVal); + formDataRef.current = { + serverUrl: serverUrlVal, + username: usernameVal, + password: passwordVal, + savePassword: savePasswordVal, + showPassword: showPasswordVal }; - }, [updateAuthFormData]); + }, [canGoBack, getAndClearLastSavedServerUrlFromSettings]); - //При готовности БД один раз подставляем сохранённый логин в поле (если оно пустое) + //При готовности БД один раз подставляем сохранённый логин в поле (только если в форме ещё нет логина) React.useEffect(() => { if (!isDbReady || savedLoginFilledRef.current) { return; } + const storedForm = getAuthFormStore(); + if (storedForm.username && String(storedForm.username).trim()) { + return; + } savedLoginFilledRef.current = true; let cancelled = false; getSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN) @@ -133,8 +189,7 @@ function AuthScreen() { }; }, [isDbReady, getSetting]); - //Сброс формы и загрузка credentials при переходе по кнопке "Вход" из меню или при открытии экрана в оффлайн - //fromMenuKey в deps обеспечивает повторную загрузку при каждом новом переходе из меню + //Сброс формы и загрузка credentials при открытии из меню или в оффлайн React.useEffect(() => { const shouldLoadFromMenu = isFromMenu && isDbReady; const shouldLoadOffline = mode === APP_MODE.OFFLINE && isDbReady; @@ -146,6 +201,7 @@ function AuthScreen() { try { if (isFromMenu) { clearAuthFormData(); + clearAuthFormStore(); } //Загружаем сохранённый URL сервера @@ -156,6 +212,7 @@ function AuthScreen() { setServerUrl(savedServerUrl); } + setSavedServerUrlFromSettings(savedServerUrl || ''); setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true); //Загружаем сохранённые credentials @@ -215,11 +272,38 @@ function AuthScreen() { loadSavedLoginOnly(); }, [isDbReady, username, isFromMenu, mode, APP_MODE.OFFLINE, getSetting]); - //Загрузка настроек при готовности БД - //В т.ч. при работе в оффлайн подставляем сохранённый логин и при необходимости пароль + //Загрузка адреса сервера и настройки видимости поля (при возврате на экран форму не перезаписываем) React.useEffect(() => { - //Пропускаем если БД не готова, уже загружали или открыто из меню - if (!isDbReady || initialLoadRef.current || isFromMenu) { + if (!isDbReady || currentScreen !== SCREENS.AUTH) { + return; + } + + const loadServerAndVisibility = async () => { + try { + const savedUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL); + const savedHide = await getSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL); + const trimmedUrl = savedUrl && String(savedUrl).trim() ? String(savedUrl).trim() : ''; + if (trimmedUrl && !canGoBack) { + setServerUrl(trimmedUrl); + } + setSavedServerUrlFromSettings(trimmedUrl); + setHideServerUrl(savedHide === 'true' || savedHide === true); + } catch (e) { + console.warn('Загрузка адреса сервера и настройки видимости:', e); + } + }; + + loadServerAndVisibility(); + }, [isDbReady, currentScreen, SCREENS.AUTH, canGoBack, getSetting]); + + //Загрузка настроек при готовности БД (только при первом открытии экрана; не перезаписываем форму, если в store уже есть введённые данные — возврат из настроек) + React.useEffect(() => { + if (!isDbReady || initialLoadRef.current || isFromMenu || canGoBack) { + return; + } + const stored = getAuthFormStore(); + if ((stored.username && String(stored.username).trim()) || (stored.password && String(stored.password).length > 0)) { + setIsSettingsLoaded(true); return; } @@ -230,16 +314,15 @@ function AuthScreen() { const savedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL); const savedHideServerUrl = await getSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL); - //Получаем текущие значения формы const currentFormData = formDataRef.current; if (savedServerUrl && !currentFormData.serverUrl) { setServerUrl(savedServerUrl); } + setSavedServerUrlFromSettings(savedServerUrl || ''); setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true); - //Загружаем сохранённые credentials (логин/пароль) для отображения в т.ч. в оффлайн const savedCredentials = await getSavedCredentials(); if (savedCredentials && savedCredentials.login) { setUsername(savedCredentials.login); @@ -263,7 +346,7 @@ function AuthScreen() { }; loadSettings(); - }, [isDbReady, isFromMenu, getSetting, getSavedCredentials]); + }, [isDbReady, isFromMenu, canGoBack, getSetting, getSavedCredentials]); //Валидация формы const validateForm = React.useCallback(() => { @@ -323,13 +406,13 @@ function AuthScreen() { return; } - showSuccess(LOGIN_SUCCESS_MESSAGE); setOnline(); clearAuthFormData(); + clearAuthFormStore(); reset(); - }, [login, serverUrl, username, password, savePassword, getSetting, showError, showSuccess, setOnline, clearAuthFormData, reset]); + }, [login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, reset]); - //Обработчик входа: проверка смены параметров подключения, при необходимости — предупреждение и сброс локальных данных + //Обработчик входа (очистка локальных данных только при смене сервера относительно последнего успешного подключения) const handleLogin = React.useCallback(async () => { if (!validateForm()) { return; @@ -337,24 +420,24 @@ function AuthScreen() { const currentNormalized = normalizeServerUrl(serverUrl); const prevSession = await getAuthSession(); - const savedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL).catch(() => ''); - const prevServerUrl = prevSession?.serverUrl || savedServerUrl; - const prevNormalized = normalizeServerUrl(prevServerUrl); + const lastConnectedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.LAST_CONNECTED_SERVER_URL).catch(() => ''); + const prevServerUrl = prevSession?.serverUrl || lastConnectedServerUrl || ''; + const prevNormalized = prevServerUrl ? normalizeServerUrl(prevServerUrl) : ''; const connectionParamsChanged = prevNormalized !== '' && currentNormalized !== prevNormalized; if (connectionParamsChanged) { - showInfo('При смене параметров подключения все локальные данные будут сброшены. Продолжить?', { - title: 'Подтверждение входа', + showInfo(AUTH_SERVER_CHANGE_CONFIRM_MESSAGE, { + title: AUTH_SERVER_CHANGE_CONFIRM_TITLE, buttons: [ { id: 'cancel', - title: 'Отмена', + title: AUTH_SERVER_CHANGE_CANCEL_BUTTON, onPress: () => {} }, { id: 'confirm', - title: 'Продолжить', + title: AUTH_SERVER_CHANGE_CONFIRM_BUTTON, onPress: async () => { await clearAuthSession(); await clearInspections(); @@ -392,20 +475,18 @@ function AuthScreen() { } //Успешный вход - showSuccess(LOGIN_SUCCESS_MESSAGE); setOnline(); //Очищаем временные данные setPendingLoginData(null); setOrganizations([]); - - //Очищаем данные формы в контексте clearAuthFormData(); + clearAuthFormStore(); //Сбрасываем навигацию reset(); }, - [pendingLoginData, selectCompany, showError, showSuccess, setOnline, clearAuthFormData, reset] + [pendingLoginData, selectCompany, showError, setOnline, clearAuthFormData, reset] ); //Обработчик отмены выбора организации @@ -424,11 +505,15 @@ function AuthScreen() { //Обработчик переключения показа пароля const handleTogglePassword = React.useCallback(value => { + formDataRef.current.showPassword = value; + setAuthFormStore({ showPassword: value }); setShowPassword(value); }, []); //Обработчик переключения сохранения пароля const handleToggleSavePassword = React.useCallback(value => { + formDataRef.current.savePassword = value; + setAuthFormStore({ savePassword: value }); setSavePassword(value); }, []); @@ -495,8 +580,8 @@ function AuthScreen() { ]; }, [handleOpenSettings, handleShowAbout]); - //Поле сервера показываем, если настройка выключена или сервер не указан - const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, serverUrl); + //Поле сервера показываем, если настройка выключена или в настройках ещё не сохранён адрес + const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings); return ( @@ -528,12 +613,12 @@ function AuthScreen() { key="server-input" label="Сервер" value={serverUrl} - onChangeText={setServerUrl} + onChangeText={handleServerUrlChange} placeholder="https://example.com/api" keyboardType="url" autoCapitalize="none" autoCorrect={false} - disabled={isLoading} + disabled={isLoading || isFromMenu} blurOnSubmit={false} returnKeyType="next" onSubmitEditing={handleServerSubmitEditing} @@ -545,7 +630,7 @@ function AuthScreen() { key="login-input" label="Логин" value={username} - onChangeText={setUsername} + onChangeText={handleUsernameChange} placeholder="Введите логин" autoCapitalize="none" autoCorrect={false} @@ -560,7 +645,7 @@ function AuthScreen() { key="password-input" label="Пароль" value={password} - onChangeText={setPassword} + onChangeText={handlePasswordChange} placeholder="Введите пароль" showPassword={showPassword} onTogglePassword={handleTogglePassword} diff --git a/rn/app/src/screens/MainScreen.js b/rn/app/src/screens/MainScreen.js index 52eb767..d446d69 100644 --- a/rn/app/src/screens/MainScreen.js +++ b/rn/app/src/screens/MainScreen.js @@ -1,6 +1,6 @@ /* Предрейсовые осмотры - мобильное приложение - Главный экран приложения с боковым меню + Главный экран приложения */ //--------------------- @@ -9,33 +9,21 @@ const React = require('react'); //React и хуки const { View } = require('react-native'); //Базовые компоненты -const { LOAD_STATUS_LOADING } = require('../config/loadStatus'); //Статусы загрузки const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД -const { useAppPreTripInspectionsContext } = require('../components/layout/AppPreTripInspectionsProvider'); //Контекст осмотров const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню const SideMenu = require('../components/menu/SideMenu'); //Боковое меню -const InspectionList = require('../components/inspections/InspectionList'); //Список осмотров -const LoadingOverlay = require('../components/common/LoadingOverlay'); //Оверлей загрузки -const OrganizationSelectDialog = require('../components/auth/OrganizationSelectDialog'); //Диалог выбора организации +const ScannerArea = require('../components/scanner/ScannerArea'); //Область сканера +const ScanResultModal = require('../components/scanner/ScanResultModal'); //Модальное окно результата сканирования +const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении -const { - CONNECTION_LOST_MESSAGE, - OFFLINE_MODE_TITLE, - APP_ABOUT_TITLE, - SIDE_MENU_TITLE, - ORGANIZATION_SELECT_DIALOG_TITLE, - MENU_ITEM_SETTINGS, - MENU_ITEM_ABOUT, - MENU_ITEM_LOGIN, - MENU_ITEM_LOGOUT, - LOGOUT_SUCCESS_MESSAGE -} = require('../config/messages'); //Сообщения +const { APP_ABOUT_TITLE, SIDE_MENU_TITLE, MENU_ITEM_SETTINGS, MENU_ITEM_ABOUT, MENU_ITEM_LOGIN, MENU_ITEM_LOGOUT } = require('../config/messages'); //Сообщения const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов const { APP_COLORS } = require('../config/theme'); //Цветовая схема +const { shouldShowScanner } = require('../config/scannerConfig'); //Логика видимости сканера const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана //----------- @@ -44,162 +32,40 @@ const styles = require('../styles/screens/MainScreen.styles'); //Стили эк //Главный экран приложения function MainScreen() { - const { inspections, loadStatus, error, isDbReady, refreshInspections } = useAppPreTripInspectionsContext(); - const { showInfo, showError, showSuccess } = useAppMessagingContext(); - const { mode, setOnline, setOffline, setNotConnected } = useAppModeContext(); + const { showInfo, showError } = useAppMessagingContext(); + const { mode, setNotConnected } = useAppModeContext(); const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext(); const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext(); - const { - session, - isAuthenticated, - isInitialized, - logout, - checkSession, - selectCompany, - isLoading: isAuthLoading, - sessionChecked, - markSessionChecked, - getAndClearSessionRestoredFromStorage - } = useAppAuthContext(); + const { session, isAuthenticated, logout, isStartupSessionCheckInProgress } = useAppAuthContext(); const [menuVisible, setMenuVisible] = React.useState(false); const [serverUrl, setServerUrl] = React.useState(''); + const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false); + const [scanResult, setScanResult] = React.useState(null); - //Состояние для диалога выбора организации при проверке сессии - const [showOrgDialog, setShowOrgDialog] = React.useState(false); - const [organizations, setOrganizations] = React.useState([]); - const [pendingSessionData, setPendingSessionData] = React.useState(null); - - //Предотвращение повторной загрузки при монтировании - const initialLoadRef = React.useRef(false); - - //Загрузка URL сервера при готовности БД + //Загрузка настроек главного экрана при готовности БД React.useEffect(() => { if (!isLocalDbReady) { return; } - const loadServerUrl = async () => { + const loadMainScreenSettings = async () => { try { - const savedUrl = await getSetting('app_server_url'); + const savedUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL); if (savedUrl) { setServerUrl(savedUrl); } + + const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER); + setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true); } catch (loadError) { - console.error('Ошибка загрузки URL сервера:', loadError); + console.error('Ошибка загрузки настроек главного экрана:', loadError); } }; - loadServerUrl(); + loadMainScreenSettings(); }, [isLocalDbReady, getSetting]); - //Проверка соединения только при открытии приложения и только если пользователь был авторизован и не выходил - React.useEffect(() => { - if (!isInitialized || sessionChecked) { - return; - } - if (!getAndClearSessionRestoredFromStorage()) { - return; - } - - markSessionChecked(); - - const verifySession = async () => { - if (!isAuthenticated) { - return; - } - - const result = await checkSession(); - - if (!result.success) { - //Сессия недействительна - showInfo('Сессия истекла. Выполните повторный вход.'); - return; - } - - //Проверяем необходимость выбора организации - if (result.needSelectCompany) { - setOrganizations(result.organizations); - setPendingSessionData({ - serverUrl: result.serverUrl, - sessionId: result.sessionId, - user: result.user, - savePassword: result.savePassword - }); - setShowOrgDialog(true); - return; - } - - //Устанавливаем режим работы на основе проверки сервера - if (result.isOffline) { - setOffline(); - showInfo(CONNECTION_LOST_MESSAGE, { title: OFFLINE_MODE_TITLE }); - } else { - setOnline(); - } - }; - - verifySession(); - }, [ - isInitialized, - isAuthenticated, - sessionChecked, - markSessionChecked, - getAndClearSessionRestoredFromStorage, - checkSession, - setOnline, - setOffline, - showInfo - ]); - - //Первичная загрузка данных - React.useEffect(() => { - //Выходим, если БД не готова или уже загружали - if (!isDbReady || initialLoadRef.current) { - return; - } - initialLoadRef.current = true; - refreshInspections(); - }, [isDbReady, refreshInspections]); - - //Обработчик выбора организации при проверке сессии - const handleSelectOrganization = React.useCallback( - async org => { - setShowOrgDialog(false); - - if (!pendingSessionData) { - return; - } - - const result = await selectCompany({ - serverUrl: pendingSessionData.serverUrl, - sessionId: pendingSessionData.sessionId, - user: pendingSessionData.user, - company: org, - savePassword: pendingSessionData.savePassword - }); - - if (!result.success) { - showError(result.error || 'Ошибка выбора организации'); - return; - } - - setOnline(); - setPendingSessionData(null); - setOrganizations([]); - }, - [pendingSessionData, selectCompany, showError, setOnline] - ); - - //Обработчик отмены выбора организации - const handleCancelOrganization = React.useCallback(() => { - setShowOrgDialog(false); - setPendingSessionData(null); - setOrganizations([]); - //При отмене остаемся в оффлайн режиме - setOffline(); - }, [setOffline]); - //Обработчик открытия меню const handleMenuOpen = React.useCallback(() => { setMenuVisible(true); @@ -233,18 +99,17 @@ function MainScreen() { navigate(SCREENS.SETTINGS); }, [navigate, SCREENS.SETTINGS]); - //Обработчик подтверждения выхода (для диалога) + //Выполнение выхода (для диалога подтверждения) const performLogout = React.useCallback(async () => { const result = await logout({ skipServerRequest: mode === 'OFFLINE' }); if (result.success) { - showSuccess(LOGOUT_SUCCESS_MESSAGE); setNotConnected(); setInitialScreen(SCREENS.AUTH); } else { showError(result.error || 'Ошибка выхода'); } - }, [logout, mode, showSuccess, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]); + }, [logout, mode, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]); //Обработчик выхода из приложения const handleLogout = React.useCallback(() => { @@ -299,17 +164,29 @@ function MainScreen() { return items; }, [handleOpenSettings, handleShowAbout, handleLogin, handleLogout, mode, isAuthenticated]); + //Обработчик результата сканирования + const handleScanResult = React.useCallback(result => { + setScanResult(result); + }, []); + + //Закрытие модального окна результата сканирования + const handleCloseScanResult = React.useCallback(() => { + setScanResult(null); + }, []); + + //Видимость сканера + const isScannerOpen = shouldShowScanner({ + alwaysShowScanner, + hasScanResult: scanResult != null, + isStartupCheckInProgress: isStartupSessionCheckInProgress + }); + return ( - + - - - ); } diff --git a/rn/app/src/screens/SettingsScreen.js b/rn/app/src/screens/SettingsScreen.js index 43a3fe4..a1194e6 100644 --- a/rn/app/src/screens/SettingsScreen.js +++ b/rn/app/src/screens/SettingsScreen.js @@ -25,7 +25,13 @@ const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConf const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов const { getAppInfo, getModeLabel } = require('../utils/appInfo'); //Информация о приложении и режиме const { validateServerUrlAllowEmpty, validateIdleTimeout } = require('../utils/validation'); //Валидация -const { APP_ABOUT_TITLE, SETTINGS_SERVER_SAVED_MESSAGE, SETTINGS_RESET_SUCCESS_MESSAGE, MENU_ITEM_ABOUT } = require('../config/messages'); //Сообщения +const { + APP_ABOUT_TITLE, + SETTINGS_SERVER_SAVED_MESSAGE, + SETTINGS_RESET_SUCCESS_MESSAGE, + MENU_ITEM_ABOUT, + SCANNER_SETTING_LABEL +} = require('../config/messages'); //Сообщения const styles = require('../styles/screens/SettingsScreen.styles'); //Стили экрана //----------- @@ -37,7 +43,7 @@ function SettingsScreen() { const { APP_MODE, mode, setNotConnected } = useAppModeContext(); const { goBack, canGoBack } = useAppNavigationContext(); const { getSetting, setSetting, clearSettings, clearInspections, vacuum, isDbReady } = useAppLocalDbContext(); - const { session, isAuthenticated, getDeviceId } = useAppAuthContext(); + const { session, isAuthenticated, getDeviceId, setLastSavedServerUrlFromSettings } = useAppAuthContext(); const [serverUrl, setServerUrl] = React.useState(''); const [hideServerUrl, setHideServerUrl] = React.useState(false); @@ -46,6 +52,7 @@ function SettingsScreen() { const [isLoading, setIsLoading] = React.useState(false); const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false); const [isIdleTimeoutDialogVisible, setIsIdleTimeoutDialogVisible] = React.useState(false); + const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false); //Предотвращение повторной загрузки настроек const settingsLoadedRef = React.useRef(false); @@ -83,6 +90,9 @@ function SettingsScreen() { //Получаем или генерируем идентификатор устройства const currentDeviceId = await getDeviceId(); setDeviceId(currentDeviceId || ''); + + const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER); + setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true); } catch (error) { console.error('Ошибка загрузки настроек:', error); showError('Не удалось загрузить настройки'); @@ -141,6 +151,9 @@ function SettingsScreen() { if (success) { setServerUrl(valueToSave); + if (valueToSave) { + setLastSavedServerUrlFromSettings(valueToSave); + } showSuccess(SETTINGS_SERVER_SAVED_MESSAGE); } else { showError('Не удалось сохранить настройки'); @@ -152,7 +165,27 @@ function SettingsScreen() { setIsLoading(false); }, - [setSetting, showError, showSuccess] + [setSetting, showError, showSuccess, setLastSavedServerUrlFromSettings] + ); + + //Переключатель «Всегда отображать сканер на главном экране» + const handleToggleAlwaysShowScanner = React.useCallback( + async value => { + try { + const success = await setSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER, value ? 'true' : 'false'); + + if (success) { + setAlwaysShowScanner(value); + showSuccess('Настройка сохранена'); + } else { + showError('Не удалось сохранить настройку'); + } + } catch (error) { + console.error('Ошибка сохранения настройки сканера:', error); + showError('Не удалось сохранить настройку'); + } + }, + [setSetting, showSuccess, showError] ); //Переключатель скрытия URL сервера в окне логина @@ -247,6 +280,7 @@ function SettingsScreen() { if (success) { setServerUrl(''); setHideServerUrl(false); + setAlwaysShowScanner(false); setIdleTimeout(defaultValue); await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue); setNotConnected(); @@ -409,6 +443,21 @@ function SettingsScreen() { + + + Главный экран + + + + + + + Системные настройки diff --git a/rn/app/src/styles/common/Backdrop.styles.js b/rn/app/src/styles/common/Backdrop.styles.js deleted file mode 100644 index 3ac2508..0000000 --- a/rn/app/src/styles/common/Backdrop.styles.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Стили компонента заднего фона -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const { StyleSheet } = require('react-native'); //StyleSheet React Native -const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения - -//----------- -//Тело модуля -//----------- - -//Стили заднего фона -const styles = StyleSheet.create({ - backdrop: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: APP_COLORS.overlay, - zIndex: 999 - } -}); - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = styles; diff --git a/rn/app/src/styles/inspections/InspectionItem.styles.js b/rn/app/src/styles/inspections/InspectionItem.styles.js deleted file mode 100644 index 4f6ed43..0000000 --- a/rn/app/src/styles/inspections/InspectionItem.styles.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - Предрейсовые осмотры - мобильное приложение - Стили элемента списка предрейсовых осмотров -*/ - -//--------------------- -//Подключение библиотек -//--------------------- - -const { StyleSheet } = require('react-native'); //StyleSheet React Native -const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения - -//----------- -//Тело модуля -//----------- - -//Стили элемента -const styles = StyleSheet.create({ - container: { - paddingHorizontal: 16, - paddingVertical: 12, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: APP_COLORS.borderSubtle, - backgroundColor: APP_COLORS.surface - }, - title: { - fontSize: 16, - fontWeight: '500', - marginBottom: 4 - }, - metaRow: { - flexDirection: 'row', - justifyContent: 'space-between' - }, - meta: { - fontSize: 12, - color: APP_COLORS.textSecondary - } -}); - -//---------------- -//Интерфейс модуля -//---------------- - -module.exports = styles; diff --git a/rn/app/src/styles/scanner/BarcodeScanner.styles.js b/rn/app/src/styles/scanner/BarcodeScanner.styles.js new file mode 100644 index 0000000..ef27a0d --- /dev/null +++ b/rn/app/src/styles/scanner/BarcodeScanner.styles.js @@ -0,0 +1,48 @@ +/* + Предрейсовые осмотры - мобильное приложение + Стили компонента сканера штрихкодов +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const { StyleSheet } = require('react-native'); //StyleSheet React Native +const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения +const { UI } = require('../../config/appConfig'); //Конфигурация UI +const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты + +//----------- +//Тело модуля +//----------- + +//Стили сканера +const styles = StyleSheet.create({ + container: { + flex: 1, + borderRadius: UI.BORDER_RADIUS, + overflow: 'hidden', + backgroundColor: APP_COLORS.black, + minHeight: responsiveSpacing(30) + }, + camera: { + flex: 1 + }, + fallbackContainer: { + flex: 1, + backgroundColor: APP_COLORS.surfaceAlt, + alignItems: 'center', + justifyContent: 'center', + padding: responsiveSpacing(4) + }, + fallbackText: { + textAlign: 'center', + color: APP_COLORS.textSecondary + } +}); + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = styles; diff --git a/rn/app/src/styles/scanner/ScanResultModal.styles.js b/rn/app/src/styles/scanner/ScanResultModal.styles.js new file mode 100644 index 0000000..80c4e02 --- /dev/null +++ b/rn/app/src/styles/scanner/ScanResultModal.styles.js @@ -0,0 +1,106 @@ +/* + Предрейсовые осмотры - мобильное приложение + Стили модального окна результата сканирования +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const { StyleSheet, Platform } = require('react-native'); //StyleSheet React Native +const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения +const { UI } = require('../../config/appConfig'); //Конфигурация UI +const { responsiveSpacing, widthPercentage } = require('../../utils/responsive'); //Адаптивные утилиты + +//----------- +//Тело модуля +//----------- + +//Стили модального окна результата сканирования +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: APP_COLORS.overlay, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: UI.PADDING + }, + container: { + width: '100%', + maxWidth: widthPercentage(90), + backgroundColor: APP_COLORS.surface, + borderRadius: UI.BORDER_RADIUS, + ...Platform.select({ + ios: { + shadowColor: APP_COLORS.black, + shadowOpacity: 0.25, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 } + }, + android: { + elevation: 10 + }, + web: { + boxShadow: '0 4px 10px rgba(0, 0, 0, 0.25)' + } + }) + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: UI.PADDING, + paddingVertical: responsiveSpacing(3), + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: APP_COLORS.borderSubtle + }, + title: { + fontSize: UI.FONT_SIZE_LG, + fontWeight: '600', + color: APP_COLORS.textPrimary, + flex: 1 + }, + closeButton: { + width: responsiveSpacing(8), + height: responsiveSpacing(8), + borderRadius: responsiveSpacing(4), + alignItems: 'center', + justifyContent: 'center', + backgroundColor: APP_COLORS.surfaceAlt + }, + closeButtonText: { + fontSize: responsiveSpacing(6), + color: APP_COLORS.textSecondary, + fontWeight: '300', + lineHeight: responsiveSpacing(8) + }, + content: { + paddingHorizontal: UI.PADDING, + paddingVertical: responsiveSpacing(4) + }, + typeLabel: { + marginBottom: responsiveSpacing(1), + color: APP_COLORS.textSecondary + }, + valueBlock: { + paddingVertical: responsiveSpacing(2), + paddingHorizontal: responsiveSpacing(3), + backgroundColor: APP_COLORS.surfaceAlt, + borderRadius: UI.BORDER_RADIUS, + marginBottom: responsiveSpacing(4) + }, + valueText: { + fontSize: UI.FONT_SIZE_MD, + color: APP_COLORS.textPrimary + }, + buttonsRow: { + flexDirection: 'row', + justifyContent: 'flex-end' + } +}); + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = styles; diff --git a/rn/app/src/styles/scanner/ScannerArea.styles.js b/rn/app/src/styles/scanner/ScannerArea.styles.js new file mode 100644 index 0000000..3c0106f --- /dev/null +++ b/rn/app/src/styles/scanner/ScannerArea.styles.js @@ -0,0 +1,48 @@ +/* + Предрейсовые осмотры - мобильное приложение + Стили области сканера на главном экране +*/ + +//--------------------- +//Подключение библиотек +//--------------------- + +const { StyleSheet } = require('react-native'); //StyleSheet React Native +const { responsiveSpacing, heightPercentage } = require('../../utils/responsive'); //Адаптивные утилиты +const { APP_COLORS } = require('../../config/theme'); //Цветовая схема +const { UI } = require('../../config/appConfig'); //Конфигурация UI + +//----------- +//Тело модуля +//----------- + +//Стили области сканера (примерно треть высоты экрана) +const styles = StyleSheet.create({ + container: { + height: heightPercentage(33), + minHeight: responsiveSpacing(30), + marginBottom: responsiveSpacing(2) + }, + modalContainer: { + flex: 1 + }, + modalCloseButton: { + position: 'absolute', + top: responsiveSpacing(4), + right: responsiveSpacing(4), + paddingHorizontal: responsiveSpacing(4), + paddingVertical: responsiveSpacing(2), + backgroundColor: APP_COLORS.overlay, + borderRadius: UI.BORDER_RADIUS + }, + modalCloseButtonText: { + color: APP_COLORS.textInverse, + fontWeight: '600' + } +}); + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = styles; diff --git a/rn/app/src/styles/inspections/InspectionList.styles.js b/rn/app/src/styles/scanner/ScannerPlaceholder.styles.js similarity index 55% rename from rn/app/src/styles/inspections/InspectionList.styles.js rename to rn/app/src/styles/scanner/ScannerPlaceholder.styles.js index ebd3ced..3800938 100644 --- a/rn/app/src/styles/inspections/InspectionList.styles.js +++ b/rn/app/src/styles/scanner/ScannerPlaceholder.styles.js @@ -1,6 +1,6 @@ /* Предрейсовые осмотры - мобильное приложение - Стили списка предрейсовых осмотров + Стили заглушки области сканера (кнопка «Сканировать») */ //--------------------- @@ -9,35 +9,25 @@ const { StyleSheet } = require('react-native'); //StyleSheet React Native const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения +const { UI } = require('../../config/appConfig'); //Конфигурация UI +const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты //----------- //Тело модуля //----------- -//Стили списка +//Стили заглушки сканера const styles = StyleSheet.create({ - centerContainer: { + container: { flex: 1, + backgroundColor: APP_COLORS.surfaceAlt, alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 32 + borderRadius: UI.BORDER_RADIUS, + minHeight: responsiveSpacing(30) }, - centerText: { - marginTop: 12, - textAlign: 'center', - color: APP_COLORS.textSecondary - }, - errorText: { - marginTop: 8, - textAlign: 'center', - color: APP_COLORS.error, - fontSize: 12 - }, - centerButton: { - marginTop: 16 - }, - indicator: { - color: APP_COLORS.primary + button: { + minWidth: responsiveSpacing(40) } }); diff --git a/rn/app/src/styles/screens/MainScreen.styles.js b/rn/app/src/styles/screens/MainScreen.styles.js index 82d4514..53c6bc2 100644 --- a/rn/app/src/styles/screens/MainScreen.styles.js +++ b/rn/app/src/styles/screens/MainScreen.styles.js @@ -9,7 +9,6 @@ const { StyleSheet } = require('react-native'); //StyleSheet React Native const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения -const { UI } = require('../../config/appConfig'); //Конфигурация UI const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты //----------- @@ -25,19 +24,6 @@ const styles = StyleSheet.create({ content: { flex: 1, paddingTop: responsiveSpacing(2) - }, - emptyState: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: UI.PADDING - }, - emptyStateText: { - textAlign: 'center', - marginBottom: responsiveSpacing(4) - }, - emptyStateButton: { - minWidth: responsiveSpacing(40) } }); diff --git a/rn/app/src/utils/appInfo.js b/rn/app/src/utils/appInfo.js index 8da97c5..d737831 100644 --- a/rn/app/src/utils/appInfo.js +++ b/rn/app/src/utils/appInfo.js @@ -3,14 +3,20 @@ Утилиты информации о приложении и режимах работы */ +//--------------------- +//Подключение библиотек +//--------------------- + +const pkg = require('../../package.json'); //Версия и описание из package.json + //--------- //Константы //--------- -//Версия приложения -const APP_VERSION = '1.0.0'; +//Версия приложения (из package.json) +const APP_VERSION = pkg.version || '1.0.0'; -//Название приложения +//Название приложения (отображаемое) const APP_NAME = 'Парус© Предрейсовые осмотры'; //Описание приложения diff --git a/rn/app/src/utils/authFormStore.js b/rn/app/src/utils/authFormStore.js new file mode 100644 index 0000000..d6b0933 --- /dev/null +++ b/rn/app/src/utils/authFormStore.js @@ -0,0 +1,57 @@ +/* + Предрейсовые осмотры - мобильное приложение + Хранилище данных формы входа +*/ + +//--------- +//Константы +//--------- + +const DEFAULT = { + serverUrl: '', + username: '', + password: '', + savePassword: false, + showPassword: false +}; + +//----------- +//Тело модуля +//----------- + +let stored = { ...DEFAULT }; + +//Флаг: после очистки не принимать следующую запись (cleanup при размонтировании не должен перезаписать очищенный store) +let skipNextSet = false; + +//Получить копию сохранённых данных формы +function getAuthFormStore() { + return { ...DEFAULT, ...stored }; +} + +//Сохранить данные формы (мержим с текущими) +function setAuthFormStore(data) { + if (skipNextSet) { + skipNextSet = false; + return; + } + if (data != null && typeof data === 'object') { + stored = { ...stored, ...data }; + } +} + +//Сбросить хранилище (например после успешного входа) +function clearAuthFormStore() { + stored = { ...DEFAULT }; + skipNextSet = true; +} + +//---------------- +//Интерфейс модуля +//---------------- + +module.exports = { + getAuthFormStore, + setAuthFormStore, + clearAuthFormStore +}; diff --git a/rn/app/src/utils/loginFormUtils.js b/rn/app/src/utils/loginFormUtils.js index e2ee6da..eae6120 100644 --- a/rn/app/src/utils/loginFormUtils.js +++ b/rn/app/src/utils/loginFormUtils.js @@ -8,9 +8,10 @@ //----------- //Определяет, нужно ли показывать поле ввода адреса сервера -function isServerUrlFieldVisible(hideServerUrl, serverUrl) { - const hasServerUrl = Boolean(serverUrl && String(serverUrl).trim()); - return !hideServerUrl || !hasServerUrl; +//Поле показывается, если настройка «не отображать» выключена ИЛИ в настройках ещё не сохранён адрес +function isServerUrlFieldVisible(hideServerUrl, savedServerUrl) { + const hasSavedUrl = Boolean(savedServerUrl && String(savedServerUrl).trim()); + return !hideServerUrl || !hasSavedUrl; } //----------------