Доработана логика работы с введенными данными на экране аутентификации. На главный экран добавлена возможность сканирования через камеру устройства.
This commit is contained in:
parent
b0adcad59e
commit
7bc9ddb898
@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
||||
@ -42,3 +42,6 @@ hermesEnabled=true
|
||||
# This allows your app to draw behind system bars for an immersive UI.
|
||||
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||
edgeToEdgeEnabled=false
|
||||
|
||||
# Vision Camera: включение сканера штрихкодов/QR (useCodeScanner)
|
||||
VisionCamera_enableCodeScanner=true
|
||||
|
||||
@ -34,6 +34,8 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Приложению нужен доступ к камере для сканирования штрихкодов и QR-кодов.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 (
|
||||
<Pressable style={[styles.backdrop, style]} onPress={handlePress}>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = Backdrop;
|
||||
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<AppText style={styles.title}>{item.title}</AppText>
|
||||
<View style={styles.metaRow}>
|
||||
<AppText style={styles.meta}>Статус: {item.status}</AppText>
|
||||
<AppText style={styles.meta}>Создан: {item.createdAt}</AppText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = InspectionItem;
|
||||
@ -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 }) => <InspectionItem item={item} />, []);
|
||||
|
||||
const keyExtractor = React.useCallback(item => item.id, []);
|
||||
|
||||
const handleRefresh = React.useCallback(() => {
|
||||
if (typeof onRefresh === 'function') onRefresh();
|
||||
}, [onRefresh]);
|
||||
|
||||
if (!hasData && isLoading) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="small" color={styles.indicator.color} />
|
||||
<AppText style={styles.centerText}>Загружаем данные...</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasData && !isLoading) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<AppText style={styles.centerText}>Нет данных предрейсовых осмотров</AppText>
|
||||
{error ? <AppText style={styles.errorText}>{error}</AppText> : null}
|
||||
<View style={styles.centerButton}>
|
||||
<AppButton title="Обновить" onPress={handleRefresh} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={inspections}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={<RefreshControl refreshing={!!isLoading} onRefresh={handleRefresh} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = InspectionList;
|
||||
@ -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
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 <AppPreTripInspectionsContext.Provider value={value}>{children}</AppPreTripInspectionsContext.Provider>;
|
||||
}
|
||||
|
||||
//Хук доступа к контексту предрейсовых осмотров
|
||||
function useAppPreTripInspectionsContext() {
|
||||
const ctx = React.useContext(AppPreTripInspectionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAppPreTripInspectionsContext должен использоваться внутри AppPreTripInspectionsProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
AppPreTripInspectionsProvider,
|
||||
useAppPreTripInspectionsContext
|
||||
};
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<StatusBar barStyle={statusBarStyle} backgroundColor={statusBarBackground} translucent={Platform.OS === 'android'} />
|
||||
<AdaptiveView style={styles.container}>{renderScreen()}</AdaptiveView>
|
||||
<LoadingOverlay visible={showStartupOverlay} message={STARTUP_CHECK_CONNECTION_MESSAGE} />
|
||||
<LoadingOverlay visible={showLogoutOverlay} message={LOGOUT_IN_PROGRESS_MESSAGE} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
32
rn/app/src/components/scanner/BarcodeScanner.js
Normal file
32
rn/app/src/components/scanner/BarcodeScanner.js
Normal file
@ -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 <BarcodeScannerWeb />;
|
||||
}
|
||||
|
||||
const BarcodeScannerNative = require('./BarcodeScannerNative');
|
||||
return <BarcodeScannerNative {...props} />;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = BarcodeScanner;
|
||||
104
rn/app/src/components/scanner/BarcodeScannerNative.js
Normal file
104
rn/app/src/components/scanner/BarcodeScannerNative.js
Normal file
@ -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 (
|
||||
<View style={styles.fallbackContainer}>
|
||||
<AppText style={styles.fallbackText} variant="body">
|
||||
{NO_CAMERA_MESSAGE}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Нет разрешения на камеру
|
||||
if (!hasPermission) {
|
||||
return (
|
||||
<View style={styles.fallbackContainer}>
|
||||
<AppText style={styles.fallbackText} variant="body">
|
||||
{PERMISSION_DENIED_MESSAGE}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Camera style={styles.camera} device={device} isActive={isActive} codeScanner={codeScanner} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = BarcodeScannerNative;
|
||||
37
rn/app/src/components/scanner/BarcodeScannerWeb.js
Normal file
37
rn/app/src/components/scanner/BarcodeScannerWeb.js
Normal file
@ -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 (
|
||||
<View style={styles.fallbackContainer}>
|
||||
<AppText style={styles.fallbackText} variant="body">
|
||||
{WEB_SCANNER_UNAVAILABLE}
|
||||
</AppText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = BarcodeScannerWeb;
|
||||
84
rn/app/src/components/scanner/ScanResultModal.js
Normal file
84
rn/app/src/components/scanner/ScanResultModal.js
Normal file
@ -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 (
|
||||
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={handleClosePress}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<AppText style={styles.title} numberOfLines={1}>
|
||||
{SCAN_RESULT_MODAL_TITLE}
|
||||
</AppText>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleClosePress} style={styles.closeButton}>
|
||||
<AppText style={styles.closeButtonText}>×</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.content}>
|
||||
<AppText style={styles.typeLabel} variant="caption" weight="medium">
|
||||
Тип: {displayType}
|
||||
</AppText>
|
||||
<View style={styles.valueBlock}>
|
||||
<AppText style={styles.valueText} selectable={true}>
|
||||
{displayValue}
|
||||
</AppText>
|
||||
</View>
|
||||
<View style={styles.buttonsRow}>
|
||||
<AppButton title={SCAN_RESULT_CLOSE_BUTTON} onPress={handleClosePress} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = ScanResultModal;
|
||||
89
rn/app/src/components/scanner/ScannerArea.js
Normal file
89
rn/app/src/components/scanner/ScannerArea.js
Normal file
@ -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 <BarcodeScanner isActive={true} onScan={handleScanFromArea} />;
|
||||
}
|
||||
return <ScannerPlaceholder onScanPress={handleOpenScanner} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{renderScannerContent()}
|
||||
|
||||
<Modal visible={scannerModalVisible} animationType="slide" onRequestClose={handleCloseScannerModal}>
|
||||
<View style={styles.modalContainer}>
|
||||
<BarcodeScanner isActive={scannerModalVisible} onScan={handleScanFromModal} />
|
||||
<Pressable
|
||||
style={styles.modalCloseButton}
|
||||
onPress={handleCloseScannerModal}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Закрыть сканер"
|
||||
>
|
||||
<AppText style={styles.modalCloseButtonText}>Закрыть</AppText>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = ScannerArea;
|
||||
39
rn/app/src/components/scanner/ScannerPlaceholder.js
Normal file
39
rn/app/src/components/scanner/ScannerPlaceholder.js
Normal file
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<AppButton title={SCAN_BUTTON_TITLE} onPress={handlePress} style={styles.button} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = ScannerPlaceholder;
|
||||
@ -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'
|
||||
};
|
||||
|
||||
//Значение времени простоя по умолчанию (минуты)
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
25
rn/app/src/config/scannerConfig.js
Normal file
25
rn/app/src/config/scannerConfig.js
Normal file
@ -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
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 }]
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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;
|
||||
89
rn/app/src/hooks/useStartupSessionCheck.js
Normal file
89
rn/app/src/hooks/useStartupSessionCheck.js
Normal file
@ -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;
|
||||
@ -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 (
|
||||
<AdaptiveView padding={false}>
|
||||
@ -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}
|
||||
|
||||
@ -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 (
|
||||
<View style={styles.container}>
|
||||
<AppHeader onMenuPress={handleMenuOpen} />
|
||||
|
||||
<View style={styles.content}>
|
||||
<InspectionList
|
||||
inspections={inspections}
|
||||
isLoading={loadStatus === LOAD_STATUS_LOADING}
|
||||
error={error}
|
||||
onRefresh={refreshInspections}
|
||||
/>
|
||||
<ScannerArea alwaysShowScanner={alwaysShowScanner} scannerOpen={isScannerOpen} onScanResult={handleScanResult} />
|
||||
</View>
|
||||
|
||||
<SideMenu
|
||||
@ -322,15 +199,12 @@ function MainScreen() {
|
||||
organization={session?.companyName}
|
||||
/>
|
||||
|
||||
<OrganizationSelectDialog
|
||||
visible={showOrgDialog}
|
||||
organizations={organizations}
|
||||
onSelect={handleSelectOrganization}
|
||||
onCancel={handleCancelOrganization}
|
||||
title={ORGANIZATION_SELECT_DIALOG_TITLE}
|
||||
<ScanResultModal
|
||||
visible={scanResult != null}
|
||||
codeType={scanResult?.type}
|
||||
value={scanResult?.value}
|
||||
onRequestClose={handleCloseScanResult}
|
||||
/>
|
||||
|
||||
<LoadingOverlay visible={isAuthLoading} message="Выполняется операция..." />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Главный экран
|
||||
</AppText>
|
||||
|
||||
<View style={styles.switchRow}>
|
||||
<AppSwitch
|
||||
label={SCANNER_SETTING_LABEL}
|
||||
value={alwaysShowScanner}
|
||||
onValueChange={handleToggleAlwaysShowScanner}
|
||||
disabled={isLoading || !isDbReady}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Системные настройки
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
48
rn/app/src/styles/scanner/BarcodeScanner.styles.js
Normal file
48
rn/app/src/styles/scanner/BarcodeScanner.styles.js
Normal file
@ -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;
|
||||
106
rn/app/src/styles/scanner/ScanResultModal.styles.js
Normal file
106
rn/app/src/styles/scanner/ScanResultModal.styles.js
Normal file
@ -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;
|
||||
48
rn/app/src/styles/scanner/ScannerArea.styles.js
Normal file
48
rn/app/src/styles/scanner/ScannerArea.styles.js
Normal file
@ -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;
|
||||
@ -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)
|
||||
}
|
||||
});
|
||||
|
||||
@ -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)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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 = 'Парус© Предрейсовые осмотры';
|
||||
|
||||
//Описание приложения
|
||||
|
||||
57
rn/app/src/utils/authFormStore.js
Normal file
57
rn/app/src/utils/authFormStore.js
Normal file
@ -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
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
//----------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user