Доработана логика работы с введенными данными на экране аутентификации. На главный экран добавлена возможность сканирования через камеру устройства.

This commit is contained in:
boa604 2026-02-27 13:56:21 +03:00
parent b0adcad59e
commit 7bc9ddb898
40 changed files with 1116 additions and 753 deletions

View File

@ -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"

View File

@ -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

View File

@ -34,6 +34,8 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Приложению нужен доступ к камере для сканирования штрихкодов и QR-кодов.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>

View File

@ -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",

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
]
);

View File

@ -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(() => {

View File

@ -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
};

View File

@ -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);

View File

@ -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} />
</>
);
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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'
};
//Значение времени простоя по умолчанию (минуты)

View File

@ -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
};

View 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
};

View File

@ -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);

View File

@ -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 {

View File

@ -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 }]
});
}, []);

View File

@ -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,

View File

@ -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;

View 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;

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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">
Системные настройки

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View 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;

View File

@ -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)
}
});

View File

@ -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)
}
});

View File

@ -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 = 'Парус© Предрейсовые осмотры';
//Описание приложения

View 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
};

View File

@ -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;
}
//----------------