Рефакторинг. Добавление иконок для пунктов меню. Изменение папки файлов приложения (с app на parus_pre_trip_inspections). Доработка работы встроенного сканера устройства.

This commit is contained in:
boa604 2026-03-10 17:51:23 +03:00
parent 7bc9ddb898
commit 5449f61c6e
77 changed files with 2807 additions and 927 deletions

View File

@ -13,8 +13,9 @@ const AppMessagingProvider = require('./src/components/layout/AppMessagingProvid
const AppNavigationProvider = require('./src/components/layout/AppNavigationProvider').AppNavigationProvider; //Провайдер навигации
const AppModeProvider = require('./src/components/layout/AppModeProvider').AppModeProvider; //Провайдер режима работы
const AppLocalDbProvider = require('./src/components/layout/AppLocalDbProvider').AppLocalDbProvider; //Провайдер локальной БД
const AppAuthProvider = require('./src/components/layout/AppAuthProvider').AppAuthProvider; //Провайдер авторизации
const AppServerProvider = require('./src/components/layout/AppServerProvider').AppServerProvider; //Провайдер сервера
const AppPreTripInspectionsProvider = require('./src/components/layout/AppPreTripInspectionsProvider').AppPreTripInspectionsProvider; //Провайдер осмотров
const { AppIdleProvider } = require('./src/components/layout/AppIdleProvider'); //Провайдер простоя
const AppRoot = require('./src/components/layout/AppRoot'); //Корневой layout приложения
//-----------
@ -28,13 +29,15 @@ function App() {
<AppMessagingProvider>
<AppNavigationProvider>
<AppLocalDbProvider>
<AppModeProvider>
<AppServerProvider>
<AppPreTripInspectionsProvider>
<AppRoot />
</AppPreTripInspectionsProvider>
</AppServerProvider>
</AppModeProvider>
<AppAuthProvider>
<AppModeProvider>
<AppServerProvider>
<AppIdleProvider>
<AppRoot />
</AppIdleProvider>
</AppServerProvider>
</AppModeProvider>
</AppAuthProvider>
</AppLocalDbProvider>
</AppNavigationProvider>
</AppMessagingProvider>

View File

@ -77,9 +77,9 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.app"
namespace "com.parus_pre_trip_inspections"
defaultConfig {
applicationId "com.app"
applicationId "com.parus_pre_trip_inspections"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
@ -117,3 +117,19 @@ dependencies {
implementation jscFlavor
}
}
// Обход: перед native clean нужны сгенерированные codegen-папки автолинкованных библиотек
afterEvaluate {
def codegenDeps = []
rootProject.subprojects.each { sub ->
def codegenTask = sub.tasks.findByName("generateCodegenArtifactsFromSchema")
if (codegenTask != null) {
codegenDeps.add(codegenTask)
}
}
tasks.configureEach { task ->
if (task.name.startsWith("externalNativeBuild") && task.name.contains("Clean")) {
codegenDeps.each { task.dependsOn(it) }
}
}
}

View File

@ -11,7 +11,8 @@
android:allowBackup="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:supportsRtl="true">
android:supportsRtl="true"
android:requestLegacyExternalStorage="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

@ -1,22 +0,0 @@
package com.app
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "Pre-Trip_Inspections"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

View File

@ -0,0 +1,101 @@
/*
Предрейсовые осмотры - мобильное приложение
Нативный модуль встроенного сканера: доставка штрихкода в JS через событие по мосту.
*/
package com.parus_pre_trip_inspections
import android.view.View
import android.view.ViewGroup
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.Arguments
class HardwareScannerModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
init {
sharedReactContext = reactApplicationContext
}
companion object {
private const val MODULE_NAME = "HardwareScannerModule"
private const val LOG_TAG = "PTI_HwScan"
const val EVENT_HARDWARE_SCANNER_DATA = "HardwareScannerData"
private const val EVENT_HARDWARE_SCANNER_TRIGGER = "HardwareScannerTriggerPressed"
private const val TRIGGER_DEBOUNCE_MS = 600L
@Volatile
var sharedReactContext: ReactApplicationContext? = null
private set
@Volatile
private var lastTriggerEmitTime = 0L
fun emitBarcodeToJs(barcode: String?) {
if (barcode.isNullOrEmpty()) return
val ctx = sharedReactContext ?: return
val payload = barcode
try {
if (!ctx.hasActiveCatalystInstance()) return
ctx.runOnUiQueueThread {
try {
if (!ctx.hasActiveCatalystInstance()) return@runOnUiQueueThread
val map = Arguments.createMap()
map.putString("barcode", payload)
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit(EVENT_HARDWARE_SCANNER_DATA, map)
} catch (_: Exception) {}
}
} catch (_: Exception) {}
}
//Эмит триггера только с UiQueueThread
fun emitTriggerFromActivity(appContext: android.content.Context?): Boolean {
val now = System.currentTimeMillis()
if (now - lastTriggerEmitTime < TRIGGER_DEBOUNCE_MS) return false
lastTriggerEmitTime = now
val ctx = sharedReactContext ?: return true
try {
if (!ctx.hasActiveCatalystInstance()) return true
ctx.runOnUiQueueThread {
try {
if (!ctx.hasActiveCatalystInstance()) return@runOnUiQueueThread
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit(EVENT_HARDWARE_SCANNER_TRIGGER, null)
} catch (_: Exception) {}
}
} catch (_: Exception) {}
return true
}
}
override fun getName(): String = MODULE_NAME
@ReactMethod
fun setCameraViewsNotFocusable() {
try {
val activity = reactApplicationContext.currentActivity ?: return
val content = activity.window?.decorView?.findViewById<View>(android.R.id.content) ?: return
setNotFocusableRecursive(content)
} catch (_: Exception) {}
}
private fun setNotFocusableRecursive(view: View) {
val name = view.javaClass.simpleName
if (name.contains("Camera", ignoreCase = true) ||
name.contains("Preview", ignoreCase = true) ||
name.contains("TextureView", ignoreCase = true) ||
name.contains("SurfaceView", ignoreCase = true)
) {
view.setFocusable(false)
view.setFocusableInTouchMode(false)
}
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
setNotFocusableRecursive(view.getChildAt(i))
}
}
}
}

View File

@ -0,0 +1,23 @@
/*
Предрейсовые осмотры - мобильное приложение
Регистрация нативного модуля встроенного сканера
*/
package com.parus_pre_trip_inspections
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
//Пакет нативного модуля встроенного сканера
class HardwareScannerPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(HardwareScannerModule(reactContext))
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}

View File

@ -0,0 +1,221 @@
/*
Предрейсовые осмотры - мобильное приложение
Главная Activity: встроенный сканер сессия по триггеру, ENTER в буфер как \n, отправка по таймауту неактивности
*/
package com.parus_pre_trip_inspections
import android.os.Handler
import android.os.Looper
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.Window
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
private val scannerBuffer = StringBuilder()
private var scannerSessionActive = false
private var lastScannerKeyTime = 0L
private var lastBarcodeSentTime = 0L
private var scannerDeviceId: Int? = null
private var dispatchFromWindowCallback = false
private val mainHandler = Handler(Looper.getMainLooper())
private fun normalizeBarcodeForSend(raw: String): String = raw.trim()
override fun onCreate(savedInstanceState: android.os.Bundle?) {
setDefaultUncaughtExceptionHandler()
super.onCreate(savedInstanceState)
installScannerKeyInterceptor()
}
private fun installScannerKeyInterceptor() {
val w = window ?: return
val orig = w.callback ?: return
w.callback = object : Window.Callback by orig {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (handleScannerKeyEvent(event)) return true
dispatchFromWindowCallback = true
return try {
orig.dispatchKeyEvent(event)
} finally {
dispatchFromWindowCallback = false
}
}
}
}
private fun setDefaultUncaughtExceptionHandler() {
val prev = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
prev?.uncaughtException(t, e)
}
}
private val sessionIdleRunnable = Runnable {
if (!scannerSessionActive) return@Runnable
if (scannerBuffer.isEmpty()) {
scannerSessionActive = false
return@Runnable
}
val barcode = normalizeBarcodeForSend(scannerBuffer.toString())
scannerBuffer.setLength(0)
scannerSessionActive = false
lastBarcodeSentTime = System.currentTimeMillis()
sendBarcodeToReactNative(barcode)
}
companion object {
private const val LOG_TAG = "PTI_HwScan"
private const val SCANNER_SESSION_TIMEOUT_MS = 2000L
private const val SESSION_IDLE_SEND_MS = 1000L
private const val POST_BARCODE_SILENT_MS = 800L
private val TRIGGER_KEY_CODES = setOf(
KeyEvent.KEYCODE_NUMPAD_DOT,
139, 140, 141, 142, 143, 144,
KeyEvent.KEYCODE_ASSIST
)
}
private fun isTriggerKey(keyCode: Int, event: KeyEvent): Boolean {
if (event.getUnicodeChar(event.metaState) != 0) return false
return keyCode in TRIGGER_KEY_CODES
}
private fun isPrintableBarcodeChar(code: Int): Boolean =
(code in 32..126) || (code in 160..255) || (code in 0x400..0x4FF) || (code in 0x370..0x3FF)
override fun getMainComponentName(): String = "Pre-Trip_Inspections"
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
//Если событие пришло через установленный Window.Callback, здесь не повторяем обработку.
if (dispatchFromWindowCallback) return super.dispatchKeyEvent(event)
if (handleScannerKeyEvent(event)) return true
return super.dispatchKeyEvent(event)
}
private fun handleScannerKeyEvent(event: KeyEvent): Boolean {
val now = System.currentTimeMillis()
val keyCode = event.keyCode
if (lastBarcodeSentTime != 0L && (now - lastBarcodeSentTime) < POST_BARCODE_SILENT_MS) return true
if (scannerSessionActive) {
if (event.action != KeyEvent.ACTION_DOWN) return true
if (keyCode == KeyEvent.KEYCODE_BACK) {
mainHandler.removeCallbacks(sessionIdleRunnable)
scannerSessionActive = false
scannerBuffer.setLength(0)
return false
}
if (lastScannerKeyTime != 0L && now - lastScannerKeyTime > SCANNER_SESSION_TIMEOUT_MS) {
mainHandler.removeCallbacks(sessionIdleRunnable)
if (scannerBuffer.isNotEmpty()) {
val barcode = normalizeBarcodeForSend(scannerBuffer.toString())
scannerBuffer.setLength(0)
scannerSessionActive = false
lastBarcodeSentTime = now
sendBarcodeToReactNative(barcode)
} else scannerSessionActive = false
return true
}
lastScannerKeyTime = now
when {
isTriggerKey(keyCode, event) -> { }
keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER -> {
scannerBuffer.append('\n')
mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
}
else -> {
val appended = appendKeyToBuffer(event)
if (appended) mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
}
}
return true
}
val fromScanner = when {
event.deviceId == -1 -> false
scannerDeviceId != null -> event.deviceId == scannerDeviceId
isTriggerKey(keyCode, event) -> true
else -> false
}
if (!fromScanner) {
mainHandler.removeCallbacks(sessionIdleRunnable)
if (scannerSessionActive) {
scannerSessionActive = false
scannerBuffer.setLength(0)
}
return false
}
if (event.action != KeyEvent.ACTION_DOWN) return true
if (keyCode == KeyEvent.KEYCODE_BACK) return false
mainHandler.removeCallbacks(sessionIdleRunnable)
scannerSessionActive = true
scannerBuffer.setLength(0)
lastScannerKeyTime = now
if (scannerDeviceId == null && event.deviceId > 0) scannerDeviceId = event.deviceId
if (isTriggerKey(keyCode, event)) {
HardwareScannerModule.emitTriggerFromActivity(applicationContext)
return true
}
val appended = appendKeyToBuffer(event)
if (appended) mainHandler.postDelayed(sessionIdleRunnable, SESSION_IDLE_SEND_MS)
return true
}
private fun appendKeyToBuffer(event: KeyEvent): Boolean {
val keyCode = event.keyCode
if (keyCode in TRIGGER_KEY_CODES && event.getUnicodeChar(event.metaState) == 0) return false
var unicode = event.getUnicodeChar(event.metaState)
if (unicode == 0 && event.action == KeyEvent.ACTION_DOWN) {
try {
val map = KeyCharacterMap.load(event.deviceId)
unicode = map.get(keyCode, event.metaState)
} catch (_: Exception) {}
}
if (unicode != 0 && Character.isBmpCodePoint(unicode)) {
scannerBuffer.append(unicode.toChar())
return true
}
if (keyCode == KeyEvent.KEYCODE_UNKNOWN || keyCode == 0) {
val label = event.displayLabel
val labelCode = label.code
if (labelCode != 0 && isPrintableBarcodeChar(labelCode)) {
scannerBuffer.append(label)
return true
}
}
when (keyCode) {
KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_7, KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9 ->
scannerBuffer.append('0' + (keyCode - KeyEvent.KEYCODE_0))
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_NUMPAD_6, KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_NUMPAD_9 ->
scannerBuffer.append('0' + (keyCode - KeyEvent.KEYCODE_NUMPAD_0))
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT -> scannerBuffer.append('-')
KeyEvent.KEYCODE_SLASH -> scannerBuffer.append('/')
KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_NUMPAD_DOT ->
if (event.getUnicodeChar(event.metaState) != 0) scannerBuffer.append('.')
in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z -> scannerBuffer.append('A' + (keyCode - KeyEvent.KEYCODE_A))
KeyEvent.KEYCODE_SPACE -> scannerBuffer.append(' ')
else -> return false
}
return true
}
private fun sendBarcodeToReactNative(barcode: String) {
HardwareScannerModule.emitBarcodeToJs(barcode)
}
}

View File

@ -1,4 +1,4 @@
package com.app
package com.parus_pre_trip_inspections
import android.app.Application
import com.facebook.react.PackageList
@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(HardwareScannerPackage())
},
)
}

View File

@ -11,10 +11,6 @@ const { AppRegistry } = require("react-native"); //Регистрация кор
const App = require("./App"); //Корневой компонент приложения
const { name: appName } = require("./app.json"); //Имя приложения
//-----------
//Тело модуля
//-----------
//Регистрация корневого компонента
AppRegistry.registerComponent(appName, () => App);

View File

@ -0,0 +1,7 @@
module.exports = {
project: {
android: {
packageName: 'com.parus_pre_trip_inspections',
},
},
};

View File

@ -159,7 +159,7 @@ function readPng(filePath) {
const inflated = zlib.inflateSync(concatenated, { chunkSize: rawSize + 1024 });
if (inflated.length < rawSize) throw new Error('PNG decompressed size too small');
// Снятие фильтров строк и приведение к RGBA
//Снятие фильтров строк и приведение к RGBA
const rgba = new Uint8Array(width * height * 4);
const stride = width * bytesPerPixel;
let prev = null;
@ -251,7 +251,7 @@ function writePngBuffer(width, height, rgba) {
const rawRows = [];
for (let y = 0; y < height; y++) {
const row = Buffer.alloc(1 + stride);
row[0] = 0; // фильтр None
row[0] = 0; //фильтр None
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
row[1 + x * 4] = rgba[i];
@ -278,11 +278,11 @@ function writePngBuffer(width, height, rgba) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // глубина цвета
ihdr[9] = 6; // тип цвета RGBA
ihdr[10] = 0; // сжатие deflate
ihdr[11] = 0; // фильтр адаптивный
ihdr[12] = 0; // без чересстрочности
ihdr[8] = 8; //глубина цвета
ihdr[9] = 6; //тип цвета RGBA
ihdr[10] = 0; //сжатие deflate
ihdr[11] = 0; //фильтр адаптивный
ihdr[12] = 0; //без чересстрочности
const out = [PNG_SIGNATURE];
writeChunk(out, 'IHDR', ihdr);

View File

@ -24,7 +24,7 @@ function getAppButtonPressableStyle(stylesRef, disabled, style) {
}
//Общая кнопка приложения
function AppButton({ title, onPress, disabled = false, style, textStyle }) {
function AppButton({ title, onPress, disabled = false, style, textStyle, focusable }) {
const handlePress = React.useCallback(() => {
if (!disabled && typeof onPress === 'function') onPress();
}, [disabled, onPress]);
@ -32,7 +32,7 @@ function AppButton({ title, onPress, disabled = false, style, textStyle }) {
const getPressableStyle = React.useMemo(() => getAppButtonPressableStyle(styles, disabled, style), [disabled, style]);
return (
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle}>
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle} focusable={focusable}>
<View style={styles.content}>
<AppText style={[styles.text, textStyle]}>{title}</AppText>
</View>

View File

@ -32,21 +32,31 @@ const AppInput = React.forwardRef(function AppInput(
style,
inputStyle,
labelStyle,
onFocus: onFocusProp,
onBlur: onBlurProp,
...restProps
},
ref
) {
const [isFocused, setIsFocused] = React.useState(false);
//Обработчик фокуса
const handleFocus = React.useCallback(() => {
setIsFocused(true);
}, []);
//Обработчик фокуса (внутренний + опциональный внешний)
const handleFocus = React.useCallback(
e => {
setIsFocused(true);
if (typeof onFocusProp === 'function') onFocusProp(e);
},
[onFocusProp]
);
//Обработчик потери фокуса
const handleBlur = React.useCallback(() => {
setIsFocused(false);
}, []);
//Обработчик потери фокуса (внутренний + опциональный внешний)
const handleBlur = React.useCallback(
e => {
setIsFocused(false);
if (typeof onBlurProp === 'function') onBlurProp(e);
},
[onBlurProp]
);
return (
<View style={[styles.container, style]}>

View File

@ -23,6 +23,28 @@ function getMessageButtonPressableStyle(stylesRef, buttonStyle) {
};
}
//Формирование массива кнопок сообщения
function getMessageButtonElements(buttons, handleClose) {
if (!Array.isArray(buttons) || buttons.length === 0) {
return [];
}
const elements = [];
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
elements.push(
<AppMessageButton
key={btn.id || btn.title}
title={btn.title}
onPress={btn.onPress}
onDismiss={handleClose}
buttonStyle={btn.buttonStyle}
textStyle={btn.textStyle}
/>
);
}
return elements;
}
//Кнопка сообщения
function AppMessageButton({ title, onPress, onDismiss, buttonStyle, textStyle }) {
//Обработчик нажатия - вызывает onPress и закрывает диалог
@ -95,20 +117,7 @@ function AppMessage({
<View style={[styles.content, contentStyle]}>
<Text style={[styles.message, messageStyle]}>{message}</Text>
</View>
{hasButtons ? (
<View style={styles.buttonsRow}>
{buttons.map(btn => (
<AppMessageButton
key={btn.id || btn.title}
title={btn.title}
onPress={btn.onPress}
onDismiss={handleClose}
buttonStyle={btn.buttonStyle}
textStyle={btn.textStyle}
/>
))}
</View>
) : null}
{hasButtons ? <View style={styles.buttonsRow}>{getMessageButtonElements(buttons, handleClose)}</View> : null}
</View>
</View>
</Modal>

View File

@ -48,8 +48,6 @@ function CopyButton({ value, onCopy, onError, disabled = false, style }) {
onCopy(value);
}
} catch (error) {
console.error('Ошибка копирования в буфер:', error);
if (typeof onError === 'function') {
onError(error);
}

View File

@ -8,44 +8,100 @@
//---------------------
const React = require('react'); //React
const { Modal, View, TextInput, Pressable } = require('react-native'); //Базовые компоненты
const { Modal, View, TextInput, Pressable, Platform, StyleSheet, BackHandler } = require('react-native'); //Базовые компоненты
const AppText = require('./AppText'); //Общий текстовый компонент
const AppButton = require('./AppButton'); //Кнопка
const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
const OVERLAY_Z = 9999;
//-----------
//Тело модуля
//-----------
//Модальное окно с полем ввода
function InputDialog({
visible,
title = 'Ввод данных',
label,
value = '',
placeholder,
keyboardType = 'default',
autoCapitalize = 'none',
confirmText = 'Сохранить',
cancelText = 'Отмена',
onConfirm,
onCancel,
validator,
errorMessage
}) {
function InputDialog(
{
visible,
title = 'Ввод данных',
label,
value = '',
placeholder,
keyboardType = 'default',
autoCapitalize = 'none',
confirmText = 'Сохранить',
cancelText = 'Отмена',
onConfirm,
onCancel,
validator,
errorMessage
},
ref
) {
//Локальное значение для редактирования
const [inputValue, setInputValue] = React.useState(value);
const inputRef = React.useRef(null);
//На Android поле ввода изначально не фокусируемо, чтобы нативный слой видел «нет фокуса» и перехватывал сканер с первого символа (как на остальных экранах)
const [inputFocusable, setInputFocusable] = React.useState(Platform.OS !== 'android');
const focusFirstEditableInput = React.useCallback(() => {
if (inputRef.current && typeof inputRef.current.focus === 'function') {
inputRef.current.focus();
}
}, []);
React.useImperativeHandle(
ref,
function exposeScannerApi() {
return {
setValueFromScanner(val) {
setInputValue(val != null ? String(val) : '');
if (Platform.OS === 'android') setInputFocusable(true);
focusFirstEditableInput();
}
};
},
[focusFirstEditableInput]
);
//После включения фокусируемости на Android — ставим фокус на поле (после setValueFromScanner)
React.useEffect(
function focusWhenFocusable() {
if (Platform.OS !== 'android' || !inputFocusable) return;
focusFirstEditableInput();
},
[inputFocusable, focusFirstEditableInput]
);
const [error, setError] = React.useState('');
const [isFocused, setIsFocused] = React.useState(false);
//Сброс значения при открытии диалога
//Сброс значения и фокусируемости только при открытии диалога
const prevVisibleRef = React.useRef(false);
React.useEffect(() => {
if (visible) {
const justOpened = visible && !prevVisibleRef.current;
prevVisibleRef.current = visible;
if (justOpened) {
setInputValue(value);
setError('');
if (Platform.OS === 'android') setInputFocusable(false);
}
}, [visible, value]);
//Кнопка «Назад» на Android при overlay (без Modal) закрывает диалог
React.useEffect(
function backHandler() {
if (Platform.OS !== 'android' || !visible) return;
const sub = BackHandler.addEventListener('hardwareBackPress', function onBack() {
handleCancel();
return true;
});
return function remove() {
sub.remove();
};
},
[visible, handleCancel]
);
//Обработчик фокуса
const handleFocus = React.useCallback(() => {
setIsFocused(true);
@ -97,54 +153,88 @@ function InputDialog({
handleCancel();
}, [handleCancel]);
return (
<Modal visible={visible} transparent={true} animationType="fade" statusBarTranslucent={true} onRequestClose={handleRequestClose}>
<View style={styles.backdrop}>
<View style={styles.container}>
<View style={styles.header}>
<AppText style={styles.title}>{title}</AppText>
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleCancel} style={styles.closeButton}>
<AppText style={styles.closeButtonText}>×</AppText>
</Pressable>
</View>
const preventKeyActions = Platform.OS === 'android';
const closePressableProps = preventKeyActions ? { focusable: false } : {};
const cancelButtonFocusable = preventKeyActions ? false : undefined;
const confirmButtonFocusable = preventKeyActions ? false : undefined;
<View style={styles.content}>
{label ? (
<AppText style={styles.label} variant="caption" weight="medium">
{label}
</AppText>
) : null}
const dialogContent = (
<View style={styles.container}>
<View style={styles.header}>
<AppText style={styles.title}>{title}</AppText>
<Pressable
accessibilityRole="button"
accessibilityLabel="Закрыть"
onPress={handleCancel}
style={styles.closeButton}
{...closePressableProps}
>
<AppText style={styles.closeButtonText}>×</AppText>
</Pressable>
</View>
<TextInput
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
value={inputValue}
onChangeText={handleChangeText}
placeholder={placeholder}
placeholderTextColor={styles.placeholder.color}
keyboardType={keyboardType}
autoCapitalize={autoCapitalize}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={true}
selectTextOnFocus={true}
accessible={true}
accessibilityLabel={label || placeholder}
accessibilityRole="text"
/>
<View style={styles.content}>
{label ? (
<AppText style={styles.label} variant="caption" weight="medium">
{label}
</AppText>
) : null}
{error ? (
<AppText style={styles.errorText} variant="caption">
{error}
</AppText>
) : null}
</View>
<TextInput
ref={inputRef}
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
value={inputValue}
onChangeText={handleChangeText}
placeholder={placeholder}
placeholderTextColor={styles.placeholder.color}
keyboardType={keyboardType}
autoCapitalize={autoCapitalize}
multiline={true}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={Platform.OS !== 'android'}
focusable={Platform.OS !== 'android' || inputFocusable}
selectTextOnFocus={true}
accessible={true}
accessibilityLabel={label || placeholder}
accessibilityRole="text"
/>
<View style={styles.buttonsRow}>
<AppButton title={cancelText} onPress={handleCancel} style={styles.cancelButton} textStyle={styles.cancelButtonText} />
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} />
</View>
{error ? (
<AppText style={styles.errorText} variant="caption">
{error}
</AppText>
) : null}
</View>
<View style={styles.buttonsRow}>
<AppButton
title={cancelText}
onPress={handleCancel}
style={styles.cancelButton}
textStyle={styles.cancelButtonText}
focusable={cancelButtonFocusable}
/>
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} focusable={confirmButtonFocusable} />
</View>
</View>
);
if (!visible) return null;
if (Platform.OS === 'android') {
return (
<View style={[StyleSheet.absoluteFill, { zIndex: OVERLAY_Z, elevation: OVERLAY_Z }]} pointerEvents="auto">
<View style={styles.backdrop} pointerEvents="auto">
{dialogContent}
</View>
</View>
);
}
return (
<Modal transparent={true} animationType="fade" statusBarTranslucent={true} visible={true} onRequestClose={handleRequestClose}>
<View style={styles.backdrop}>{dialogContent}</View>
</Modal>
);
}
@ -153,4 +243,4 @@ function InputDialog({
//Интерфейс модуля
//----------------
module.exports = InputDialog;
module.exports = React.forwardRef(InputDialog);

View File

@ -31,6 +31,8 @@ const PasswordInput = React.forwardRef(function PasswordInput(
style,
inputStyle,
labelStyle,
onFocus: onFocusProp,
onBlur: onBlurProp,
...restProps
},
ref
@ -50,15 +52,23 @@ const PasswordInput = React.forwardRef(function PasswordInput(
[ref]
);
//Обработчик фокуса
const handleFocus = React.useCallback(() => {
setIsFocused(true);
}, []);
//Обработчик фокуса (внутренний + опциональный внешний)
const handleFocus = React.useCallback(
e => {
setIsFocused(true);
if (typeof onFocusProp === 'function') onFocusProp(e);
},
[onFocusProp]
);
//Обработчик потери фокуса
const handleBlur = React.useCallback(() => {
setIsFocused(false);
}, []);
//Обработчик потери фокуса (внутренний + опциональный внешний)
const handleBlur = React.useCallback(
e => {
setIsFocused(false);
if (typeof onBlurProp === 'function') onBlurProp(e);
},
[onBlurProp]
);
//Обработчик переключения видимости пароля
const handleTogglePassword = React.useCallback(() => {

View File

@ -0,0 +1,63 @@
/*
Предрейсовые осмотры - мобильное приложение
Провайдер отслеживания простоя
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react'); //React
const { View } = require('react-native'); //Базовые компоненты
const useIdleTimeout = require('../../hooks/useIdleTimeout'); //Хук простоя
const styles = require('../../styles/layout/AppIdleProvider.styles'); //Стили обёртки
//-----------
//Тело модуля
//-----------
//Контекст простоя (reportActivity для сброса таймера)
const AppIdleContext = React.createContext(null);
//Провайдер простоя: оборачивает приложение и при отсутствии активности выполняет выход
function AppIdleProvider({ children }) {
const { reportActivity } = useIdleTimeout();
const value = React.useMemo(
() => ({
reportActivity
}),
[reportActivity]
);
//Обёртка по touch сбрасывает таймер простоя
const handleTouch = React.useCallback(() => {
reportActivity();
}, [reportActivity]);
return (
<AppIdleContext.Provider value={value}>
<View style={styles.touchArea} onTouchStart={handleTouch} onTouchEnd={handleTouch} collapsable={false}>
{children}
</View>
</AppIdleContext.Provider>
);
}
//Хук контекста простоя
function useAppIdleContext() {
const context = React.useContext(AppIdleContext);
if (context == null) {
throw new Error('useAppIdleContext должен использоваться внутри AppIdleProvider');
}
return context;
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
AppIdleProvider,
useAppIdleContext
};

View File

@ -25,16 +25,12 @@ function AppLocalDbProvider({ children }) {
const value = React.useMemo(
() => ({
isDbReady: api.isDbReady,
inspections: api.inspections,
error: api.error,
loadInspections: api.loadInspections,
saveInspection: api.saveInspection,
getSetting: api.getSetting,
setSetting: api.setSetting,
deleteSetting: api.deleteSetting,
getAllSettings: api.getAllSettings,
clearSettings: api.clearSettings,
clearInspections: api.clearInspections,
vacuum: api.vacuum,
checkTableExists: api.checkTableExists,
setAuthSession: api.setAuthSession,
@ -43,16 +39,12 @@ function AppLocalDbProvider({ children }) {
}),
[
api.isDbReady,
api.inspections,
api.error,
api.loadInspections,
api.saveInspection,
api.getSetting,
api.setSetting,
api.deleteSetting,
api.getAllSettings,
api.clearSettings,
api.clearInspections,
api.vacuum,
api.checkTableExists,
api.setAuthSession,

View File

@ -11,6 +11,7 @@ const React = require('react'); //React и хуки
const { useColorScheme, View } = require('react-native'); //Определение темы устройства и разметка
const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
const AppShell = require('./AppShell'); //Оболочка приложения
const HardwareScannerProvider = require('./HardwareScannerProvider').HardwareScannerProvider; //Встроенный сканер
const styles = require('../../styles/layout/AppRoot.styles'); //Стили корневого layout
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
@ -54,7 +55,9 @@ function AppRoot() {
return (
<SafeAreaProvider>
<View style={styles.container}>
<AppShell isDarkMode={isDarkMode} />
<HardwareScannerProvider>
<AppShell isDarkMode={isDarkMode} />
</HardwareScannerProvider>
</View>
</SafeAreaProvider>
);

View File

@ -11,6 +11,7 @@ const React = require('react'); //React
const { StatusBar, Platform } = require('react-native'); //Базовые компоненты
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
const { useAppIdleContext } = require('./AppIdleProvider'); //Контекст простоя
const MainScreen = require('../../screens/MainScreen'); //Главный экран
const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации
@ -27,6 +28,12 @@ const styles = require('../../styles/layout/AppShell.styles'); //Стили об
function AppShell({ isDarkMode }) {
const { currentScreen, SCREENS } = useAppNavigationContext();
const { isStartupSessionCheckInProgress, isLogoutInProgress } = useAppAuthContext();
const { reportActivity } = useAppIdleContext();
//Сброс таймера простоя при смене экрана (навигация считается активностью)
React.useEffect(() => {
reportActivity();
}, [currentScreen, reportActivity]);
const renderScreen = React.useCallback(() => {
switch (currentScreen) {

View File

@ -0,0 +1,97 @@
/*
Предрейсовые осмотры - мобильное приложение
Провайдер встроенного сканера: маршрутизация данных по текущему экрану
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react');
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
const HardwareScannerBridge = require('../../services/HardwareScannerBridge'); //Мост встроенного сканера
//---------
//Контекст
//---------
const HardwareScannerContext = React.createContext(null);
//-----------
//Тело модуля
//-----------
//Вызов обработчика для текущего экрана при получении данных от встроенного сканера
function dispatchHardwareScan(currentScreen, handlersRef, barcode) {
const handlers = handlersRef.current;
const hasHandler = handlers && typeof handlers[currentScreen] === 'function';
if (!handlers || !hasHandler) return;
try {
handlers[currentScreen](barcode);
} catch (_err) {}
}
//Провайдер встроенного сканера
function HardwareScannerProvider({ children }) {
const { currentScreen, SCREENS } = useAppNavigationContext();
const handlersRef = React.useRef(Object.create(null));
const currentScreenRef = React.useRef(currentScreen);
const unsubscribeRef = React.useRef(null);
currentScreenRef.current = currentScreen;
const registerHandler = React.useCallback(function registerHandler(screen, handler) {
if (!screen || typeof handler !== 'function') return;
handlersRef.current[screen] = handler;
}, []);
const unregisterHandler = React.useCallback(function unregisterHandler(screen) {
if (!screen) return;
delete handlersRef.current[screen];
}, []);
React.useEffect(function subscribeToHardwareScanner() {
const unsubscribe = HardwareScannerBridge.startHardwareListener(function onData(barcode) {
const screen = currentScreenRef.current;
dispatchHardwareScan(screen, handlersRef, barcode);
});
unsubscribeRef.current = unsubscribe;
return function cleanup() {
if (typeof unsubscribeRef.current === 'function') {
unsubscribeRef.current();
}
unsubscribeRef.current = null;
};
}, []);
const value = React.useMemo(
function getValue() {
return {
registerHandler,
unregisterHandler,
SCREENS
};
},
[registerHandler, unregisterHandler, SCREENS]
);
return <HardwareScannerContext.Provider value={value}>{children}</HardwareScannerContext.Provider>;
}
//Хук доступа к контексту встроенного сканера
function useHardwareScannerContext() {
const ctx = React.useContext(HardwareScannerContext);
if (!ctx) {
throw new Error('useHardwareScannerContext должен использоваться внутри HardwareScannerProvider');
}
return ctx;
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
HardwareScannerProvider,
useHardwareScannerContext
};

View File

@ -0,0 +1,136 @@
/*
Предрейсовые осмотры - мобильное приложение
Компонент иконки пункта бокового меню
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react'); //React
const { View } = require('react-native'); //Базовые компоненты
const AppText = require('../common/AppText'); //Текст приложения
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGIN, MENU_ITEM_ID_LOGOUT } = require('../../config/menuItemIds'); //ID пунктов меню
const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
const styles = require('../../styles/menu/MenuItemIcon.styles'); //Стили иконки
//-----------
//Тело модуля
//-----------
//Количество зубцов шестерёнки и угол шага (градусы)
const SETTINGS_TEETH_COUNT = 6;
const SETTINGS_TEETH_ANGLE_STEP = 360 / SETTINGS_TEETH_COUNT;
//Формирование массива элементов зубцов шестерёнки
function getTeethViews(teeth, iconColor) {
const views = [];
for (let index = 0; index < teeth.length; index++) {
const angle = teeth[index];
views.push(
<View
key={`tooth-${index}`}
style={[
styles.settingsTooth,
{ backgroundColor: iconColor },
{
transform: [{ rotate: `${angle}deg` }, { translateY: -responsiveSize(8) }]
}
]}
/>
);
}
return views;
}
//Иконка «Настройки» (шестерёнка)
function SettingsIcon({ color }) {
const iconColor = color ?? APP_COLORS.textPrimary;
const teeth = React.useMemo(() => {
return Array.from({ length: SETTINGS_TEETH_COUNT }, (_, i) => i * SETTINGS_TEETH_ANGLE_STEP);
}, []);
const toothViews = React.useMemo(() => getTeethViews(teeth, iconColor), [teeth, iconColor]);
return (
<View style={styles.container}>
<View style={[styles.settingsCircle, { borderColor: iconColor }]} />
{toothViews}
</View>
);
}
//Иконка «О приложении» (информация)
function AboutIcon({ color }) {
const iconColor = color ?? APP_COLORS.textPrimary;
return (
<View style={styles.container}>
<View style={[styles.aboutCircle, { borderColor: iconColor }]}>
<AppText style={[styles.aboutText, { color: iconColor }]}>i</AppText>
</View>
</View>
);
}
//Иконка «Вход»
function LoginIcon({ color }) {
const iconColor = color ?? APP_COLORS.textPrimary;
return (
<View style={styles.container}>
<View style={[styles.loginFrame, { borderColor: iconColor }]}>
<View style={[styles.loginArrow, { borderLeftColor: iconColor }]} />
</View>
</View>
);
}
//Иконка «Выход»
function LogoutIcon({ color }) {
const iconColor = color ?? APP_COLORS.textPrimary;
return (
<View style={styles.container}>
<View style={[styles.logoutFrame, { borderColor: iconColor }]}>
<View style={[styles.logoutArrow, { borderRightColor: iconColor }]} />
</View>
</View>
);
}
//Рендер иконки по имени пункта меню
function renderIconByName(name, color) {
switch (name) {
case MENU_ITEM_ID_SETTINGS:
return <SettingsIcon color={color} />;
case MENU_ITEM_ID_ABOUT:
return <AboutIcon color={color} />;
case MENU_ITEM_ID_LOGIN:
return <LoginIcon color={color} />;
case MENU_ITEM_ID_LOGOUT:
return <LogoutIcon color={color} />;
default:
return null;
}
}
//Компонент иконки пункта меню
function MenuItemIcon({ name, color }) {
const iconElement = React.useMemo(() => renderIconByName(name, color), [name, color]);
if (iconElement == null) {
return null;
}
return iconElement;
}
//----------------
//Интерфейс модуля
//----------------
module.exports = MenuItemIcon;
module.exports.renderIconByName = renderIconByName;

View File

@ -10,25 +10,57 @@
const React = require('react'); //React
const { ScrollView, View } = require('react-native'); //Базовые компоненты
const MenuItem = require('./MenuItem'); //Элемент меню
const MenuItemIcon = require('./MenuItemIcon'); //Иконка пункта меню
const EmptyMenu = require('./EmptyMenu'); //Пустое меню
const MenuDivider = require('./MenuDivider'); //Разделитель меню
const { MENU_ITEM_ID_DIVIDER } = require('../../config/menuItemIds'); //ID разделителя
const styles = require('../../styles/menu/MenuList.styles'); //Стили списка меню
//-----------
//Тело модуля
//-----------
//Иконка для пункта меню
function resolveItemIcon(item) {
if (item.icon != null) {
return item.icon;
}
if (item.id != null && item.id !== MENU_ITEM_ID_DIVIDER) {
return <MenuItemIcon name={item.id} color={item.iconColor} />;
}
return null;
}
//Формирование массива элементов списка меню
function getMenuListElements(items, onItemPress) {
if (!Array.isArray(items) || items.length === 0) {
return [];
}
const elements = [];
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (item.type === 'divider') {
elements.push(<MenuDivider key={item.id || `menu-divider-${index}`} />);
} else {
elements.push(<MenuItemRow key={item.id || `menu-item-${index}`} item={item} index={index} items={items} onItemPress={onItemPress} />);
}
}
return elements;
}
//Строка меню с элементом
function MenuItemRow({ item, index, items, onItemPress }) {
const handlePress = React.useCallback(() => {
onItemPress(item);
}, [item, onItemPress]);
const icon = React.useMemo(() => resolveItemIcon(item), [item]);
return (
<View>
<MenuItem
title={item.title}
icon={item.icon}
icon={icon}
onPress={handlePress}
isDestructive={item.isDestructive}
disabled={item.disabled}
@ -54,21 +86,15 @@ function MenuList({ items = [], onClose, style }) {
[onClose]
);
const listElements = React.useMemo(() => getMenuListElements(items, handleItemPress), [items, handleItemPress]);
if (!Array.isArray(items) || items.length === 0) {
return <EmptyMenu />;
}
return (
<ScrollView style={[styles.scrollView, style]} showsVerticalScrollIndicator={false} bounces={false}>
{items.map((item, index) => {
//Элемент-разделитель
if (item.type === 'divider') {
return <MenuDivider key={item.id || `menu-divider-${index}`} />;
}
//Обычный элемент меню
return <MenuItemRow key={item.id || `menu-item-${index}`} item={item} index={index} items={items} onItemPress={handleItemPress} />;
})}
{listElements}
</ScrollView>
);
}

View File

@ -8,7 +8,7 @@
//---------------------
const React = require('react'); //React и хуки
const { View } = require('react-native'); //Базовые компоненты
const { View, Platform } = 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'); //Конфиг сканера
@ -68,10 +68,12 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
}
}, [hasPermission, requestPermission]);
const noFocusProps = Platform.OS === 'android' ? { focusable: false } : {};
//Нет устройства камеры
if (device == null) {
return (
<View style={styles.fallbackContainer}>
<View style={styles.fallbackContainer} {...noFocusProps}>
<AppText style={styles.fallbackText} variant="body">
{NO_CAMERA_MESSAGE}
</AppText>
@ -82,7 +84,7 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
//Нет разрешения на камеру
if (!hasPermission) {
return (
<View style={styles.fallbackContainer}>
<View style={styles.fallbackContainer} {...noFocusProps}>
<AppText style={styles.fallbackText} variant="body">
{PERMISSION_DENIED_MESSAGE}
</AppText>
@ -90,8 +92,10 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
);
}
const containerProps = Platform.OS === 'android' ? { focusable: false, importantForAccessibility: 'no-hide-descendants' } : {};
return (
<View style={styles.container}>
<View style={styles.container} {...containerProps}>
<Camera style={styles.camera} device={device} isActive={isActive} codeScanner={codeScanner} />
</View>
);

View File

@ -0,0 +1,34 @@
/*
Предрейсовые осмотры - мобильное приложение
Оверлей области камеры на паузе
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react'); //React
const { View } = require('react-native'); //Базовые компоненты
const AppText = require('../common/AppText'); //Текст
const styles = require('../../styles/scanner/CameraPausedOverlay.styles'); //Стили оверлея
//-----------
//Тело модуля
//-----------
//Оверлей паузы камеры: серая область с прозрачностью и текстом-подсказкой
function CameraPausedOverlay({ message }) {
return (
<View style={styles.container} pointerEvents="box-none">
<AppText style={styles.message} variant="body" numberOfLines={3}>
{message}
</AppText>
</View>
);
}
//----------------
//Интерфейс модуля
//----------------
module.exports = CameraPausedOverlay;

View File

@ -8,10 +8,11 @@
//---------------------
const React = require('react'); //React
const { Modal, View, Pressable } = require('react-native'); //Базовые компоненты
const { Modal, View, Pressable, Platform } = 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 { noop } = require('../../utils/noop'); //Пустая функция
const styles = require('../../styles/scanner/ScanResultModal.styles'); //Стили модального окна
//-----------
@ -37,42 +38,89 @@ function handleClose(onRequestClose) {
}
}
//Пропсы для элементов, закрывающих модальное окно: на Android отключаем фокус, чтобы Enter от встроенного сканера не вызывал закрытие
function getClosePressableProps(preventKeyClose) {
return preventKeyClose ? { focusable: false } : {};
}
//Общий контент окна результата
function ResultContent({ displayType, displayValue, onClosePress, styles: s, preventKeyClose }) {
const backdropProps = getClosePressableProps(preventKeyClose);
const closeButtonProps = getClosePressableProps(preventKeyClose);
const closeButtonFocusable = preventKeyClose ? false : undefined;
return (
<Pressable style={s.backdrop} onPress={onClosePress} {...backdropProps}>
<Pressable style={s.container} onPress={noop} focusable={false}>
<View style={s.header}>
<AppText style={s.title} numberOfLines={1}>
{SCAN_RESULT_MODAL_TITLE}
</AppText>
<Pressable
accessibilityRole="button"
accessibilityLabel="Закрыть"
onPress={onClosePress}
style={s.closeButton}
{...closeButtonProps}
>
<AppText style={s.closeButtonText}>×</AppText>
</Pressable>
</View>
<View style={s.content}>
<AppText style={s.typeLabel} variant="caption" weight="medium">
Тип: {displayType}
</AppText>
<View style={s.valueBlock}>
<AppText style={s.valueText} selectable={true}>
{displayValue}
</AppText>
</View>
<View style={s.buttonsRow}>
<AppButton title={SCAN_RESULT_CLOSE_BUTTON} onPress={onClosePress} focusable={closeButtonFocusable} />
</View>
</View>
</Pressable>
</Pressable>
);
}
//Модальное окно результата сканирования
function ScanResultModal({ visible, codeType, value, onRequestClose }) {
const handleClosePress = React.useCallback(() => {
handleClose(onRequestClose);
}, [onRequestClose]);
const modalOnRequestClose = React.useMemo(() => (Platform.OS === 'android' ? noop : handleClosePress), [handleClosePress]);
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>
const preventKeyClose = Platform.OS === 'android';
const content = (
<ResultContent
displayType={displayType}
displayValue={displayValue}
onClosePress={handleClosePress}
styles={styles}
preventKeyClose={preventKeyClose}
/>
);
if (Platform.OS === 'android') {
if (!visible) return null;
return (
<View style={styles.overlay} focusable={false} importantForAccessibility="no-hide-descendants" collapsable={false}>
<View style={styles.overlayContent} pointerEvents="box-none">
{content}
</View>
</View>
);
}
return (
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={modalOnRequestClose} statusBarTranslucent={true}>
<View style={styles.modalRoot}>{content}</View>
</Modal>
);
}

View File

@ -8,10 +8,12 @@
//---------------------
const React = require('react'); //React
const { View, Modal, Pressable } = require('react-native'); //Базовые компоненты
const { View, Modal, Pressable, Platform } = require('react-native'); //Базовые компоненты
const BarcodeScanner = require('./BarcodeScanner'); //Сканер
const ScannerPlaceholder = require('./ScannerPlaceholder'); //Заглушка с кнопкой
const CameraPausedOverlay = require('./CameraPausedOverlay'); //Оверлей «камера на паузе»
const AppText = require('../common/AppText'); //Текст
const { SCANNER_CAMERA_TAP_TO_RESUME } = require('../../config/messages'); //Сообщения
const styles = require('../../styles/scanner/ScannerArea.styles'); //Стили области
//-----------
@ -29,7 +31,16 @@ function handleScanResult(result, onScanResult, closeModal) {
}
//Область сканера: при включённой настройке — активный сканер (если открыт), иначе заглушка и модальный сканер по кнопке
function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResult }) {
function ScannerArea({
alwaysShowScanner = false,
scannerOpen = true,
cameraActive = true,
onScanResult,
onResumeCameraPress,
placeholderHint,
placeholderSecondaryLabel,
placeholderSecondaryOnPress
}) {
const [scannerModalVisible, setScannerModalVisible] = React.useState(false);
const handleScanFromArea = React.useCallback(
@ -56,9 +67,33 @@ function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResu
const renderScannerContent = () => {
if (alwaysShowScanner && scannerOpen) {
return <BarcodeScanner isActive={true} onScan={handleScanFromArea} />;
const wrapperProps = Platform.OS === 'android' ? { focusable: false, importantForAccessibility: 'no-hide-descendants' } : {};
const showPausedOverlay = !cameraActive && typeof onResumeCameraPress === 'function';
return (
<View style={styles.scannerWrapper} {...wrapperProps}>
<BarcodeScanner isActive={cameraActive} onScan={handleScanFromArea} />
{showPausedOverlay ? (
<Pressable
style={styles.cameraPausedOverlayTouchable}
onPress={onResumeCameraPress}
accessibilityRole="button"
accessibilityLabel="Включить камеру"
{...(Platform.OS === 'android' ? { focusable: false } : {})}
>
<CameraPausedOverlay message={SCANNER_CAMERA_TAP_TO_RESUME} />
</Pressable>
) : null}
</View>
);
}
return <ScannerPlaceholder onScanPress={handleOpenScanner} />;
return (
<ScannerPlaceholder
onScanPress={handleOpenScanner}
hintText={placeholderHint}
secondaryButtonTitle={placeholderSecondaryLabel}
secondaryButtonOnPress={placeholderSecondaryOnPress}
/>
);
};
return (

View File

@ -10,6 +10,7 @@
const React = require('react'); //React
const { View } = require('react-native'); //Базовые компоненты
const AppButton = require('../common/AppButton'); //Кнопка
const AppText = require('../common/AppText'); //Текст
const { SCAN_BUTTON_TITLE } = require('../../config/messages'); //Сообщения
const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Стили заглушки
@ -17,17 +18,31 @@ const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Ст
//Тело модуля
//-----------
//Заглушка: затемнённая область и кнопка открытия сканера
function ScannerPlaceholder({ onScanPress }) {
//Заглушка: затемнённая область и кнопка открытия сканера; опционально подсказка и вторая кнопка (режим встроенного сканера)
function ScannerPlaceholder({ onScanPress, hintText, secondaryButtonTitle, secondaryButtonOnPress }) {
const handlePress = React.useCallback(() => {
if (typeof onScanPress === 'function') {
onScanPress();
}
}, [onScanPress]);
const handleSecondary = React.useCallback(() => {
if (typeof secondaryButtonOnPress === 'function') {
secondaryButtonOnPress();
}
}, [secondaryButtonOnPress]);
return (
<View style={styles.container}>
{hintText ? (
<AppText style={styles.hint} variant="body" numberOfLines={2}>
{hintText}
</AppText>
) : null}
<AppButton title={SCAN_BUTTON_TITLE} onPress={handlePress} style={styles.button} />
{secondaryButtonTitle && typeof secondaryButtonOnPress === 'function' ? (
<AppButton title={secondaryButtonTitle} onPress={handleSecondary} style={styles.secondaryButton} />
) : null}
</View>
);
}

View File

@ -32,7 +32,7 @@ const UI = {
//Высоты элементов
BUTTON_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
INPUT_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
INPUT_HEIGHT: responsiveSize(56),
HEADER_HEIGHT: responsiveSize(isTablet() ? 80 : Platform.OS === 'ios' ? 70 : 56)
};
@ -43,7 +43,7 @@ const COMPATIBILITY = {
if (Platform.OS !== 'android') return true;
const majorVersion = parseInt(Platform.Version, 10);
return majorVersion >= 24; // Android 7.0 = API 24
return majorVersion >= 24; //Android 7.0 = API 24
},
//Проверяем версию iOS
@ -51,7 +51,7 @@ const COMPATIBILITY = {
if (Platform.OS !== 'ios') return true;
const majorVersion = parseInt(Platform.Version, 10);
return majorVersion >= 11; // iOS 11.0
return majorVersion >= 11; //iOS 11.0
},
//Общая проверка совместимости

View File

@ -13,16 +13,18 @@ const AUTH_SETTINGS_KEYS = {
LAST_CONNECTED_SERVER_URL: 'auth_last_connected_server_url',
HIDE_SERVER_URL: 'auth_hide_server_url',
IDLE_TIMEOUT: 'auth_idle_timeout',
SERVER_REQUEST_TIMEOUT: 'auth_server_request_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',
ALWAYS_SHOW_SCANNER: 'main_always_show_scanner'
ALWAYS_SHOW_SCANNER: 'main_always_show_scanner',
MAIN_SCANNER_PRIORITY: 'main_scanner_priority'
};
//Значение времени простоя по умолчанию (минуты)
const DEFAULT_IDLE_TIMEOUT = 30;
//Значение времени ожидания ответа от сервера по умолчанию (секунды)
const DEFAULT_SERVER_REQUEST_TIMEOUT = 60;
//Ключи настроек подключения
const CONNECTION_SETTINGS_KEYS = [
@ -42,5 +44,5 @@ const CONNECTION_SETTINGS_KEYS = [
module.exports = {
AUTH_SETTINGS_KEYS,
CONNECTION_SETTINGS_KEYS,
DEFAULT_IDLE_TIMEOUT
DEFAULT_SERVER_REQUEST_TIMEOUT
};

View File

@ -0,0 +1,27 @@
/*
Предрейсовые осмотры - мобильное приложение
Конфигурация полей формы аутентификации
*/
//---------
//Константы
//---------
//Порядок полей формы аутентификации
const AUTH_FORM_FIELD_ORDER = ['server', 'login', 'password'];
//Идентификаторы полей
const AUTH_FORM_FIELD_ID = {
SERVER: 'server',
LOGIN: 'login',
PASSWORD: 'password'
};
//----------------
//Интерфейс модуля
//----------------
module.exports = {
AUTH_FORM_FIELD_ORDER,
AUTH_FORM_FIELD_ID
};

View File

@ -0,0 +1,19 @@
/*
Предрейсовые осмотры - мобильное приложение
Конфигурация совместной работы устройств ввода (клавиатура, встроенный сканер)
*/
//---------
//Константы
//---------
//Режим подстановки данных сканера в поле: всегда замена текущего значения
const SCAN_INPUT_MODE_REPLACE = 'replace';
//----------------
//Интерфейс модуля
//----------------
module.exports = {
SCAN_INPUT_MODE_REPLACE
};

View File

@ -0,0 +1,35 @@
/*
Предрейсовые осмотры - мобильное приложение
Идентификаторы пунктов бокового меню
*/
//---------
//Константы
//---------
//Пункт меню «Настройки»
const MENU_ITEM_ID_SETTINGS = 'settings';
//Пункт меню «О приложении»
const MENU_ITEM_ID_ABOUT = 'about';
//Пункт меню «Вход»
const MENU_ITEM_ID_LOGIN = 'login';
//Пункт меню «Выход»
const MENU_ITEM_ID_LOGOUT = 'logout';
//Разделитель в меню (не пункт, служебный тип)
const MENU_ITEM_ID_DIVIDER = 'divider';
//----------------
//Интерфейс модуля
//----------------
module.exports = {
MENU_ITEM_ID_SETTINGS,
MENU_ITEM_ID_ABOUT,
MENU_ITEM_ID_LOGIN,
MENU_ITEM_ID_LOGOUT,
MENU_ITEM_ID_DIVIDER
};

View File

@ -13,6 +13,9 @@ const CONNECTION_LOST_MESSAGE = 'Нет связи с сервером. Прил
//Заголовок сообщения при переходе в режим офлайн
const OFFLINE_MODE_TITLE = 'Режим офлайн';
//Сообщение при переходе в офлайн по таймауту простоя
const IDLE_TIMEOUT_OFFLINE_MESSAGE = 'Сессия закрыта по таймауту простоя. Приложение переведено в режим офлайн.';
//Сообщение при проверке соединения при старте приложения
const STARTUP_CHECK_CONNECTION_MESSAGE = 'Проверка соединения...';
@ -35,7 +38,10 @@ const MENU_ITEM_LOGOUT = 'Выход';
const AUTH_SCREEN_TITLE = 'Вход в приложение';
const AUTH_BUTTON_LOGIN = 'Войти';
const AUTH_BUTTON_LOADING = 'Вход...';
const AUTH_BUTTON_LOGOUT = 'Выйти';
const LOGOUT_IN_PROGRESS_MESSAGE = 'Выход...';
const LOGOUT_CONFIRM_TITLE = 'Подтверждение выхода';
const LOGOUT_CONFIRM_MESSAGE = 'Вы уверены, что хотите выйти?';
//Диалог подтверждения при смене сервера (относительно последнего успешного подключения)
const AUTH_SERVER_CHANGE_CONFIRM_TITLE = 'Подтверждение входа';
@ -52,10 +58,16 @@ const SCREEN_TITLE_SETTINGS = 'Настройки';
//Сканер на главном экране
const SCANNER_SETTING_LABEL = 'Всегда отображать сканер на главном экране';
const SCANNER_PRIORITY_LABEL = 'Приоритет на главном экране';
const SCANNER_PRIORITY_CAMERA = 'Камера';
const SCANNER_PRIORITY_HARDWARE = 'Встроенный сканер';
const SCAN_BUTTON_TITLE = 'Сканировать';
const SCAN_RESULT_MODAL_TITLE = 'Результат сканирования';
const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
//Оверлей паузы камеры на главном экране: подсказка включить камеру по тапу
const SCANNER_CAMERA_TAP_TO_RESUME = 'Нажмите на область камеры для её включения';
//----------------
//Интерфейс модуля
//----------------
@ -63,6 +75,7 @@ const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
module.exports = {
CONNECTION_LOST_MESSAGE,
OFFLINE_MODE_TITLE,
IDLE_TIMEOUT_OFFLINE_MESSAGE,
STARTUP_CHECK_CONNECTION_MESSAGE,
APP_ABOUT_TITLE,
SIDE_MENU_TITLE,
@ -74,7 +87,10 @@ module.exports = {
AUTH_SCREEN_TITLE,
AUTH_BUTTON_LOGIN,
AUTH_BUTTON_LOADING,
AUTH_BUTTON_LOGOUT,
LOGOUT_IN_PROGRESS_MESSAGE,
LOGOUT_CONFIRM_TITLE,
LOGOUT_CONFIRM_MESSAGE,
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
@ -83,7 +99,11 @@ module.exports = {
SETTINGS_RESET_SUCCESS_MESSAGE,
SCREEN_TITLE_SETTINGS,
SCANNER_SETTING_LABEL,
SCANNER_PRIORITY_LABEL,
SCANNER_PRIORITY_CAMERA,
SCANNER_PRIORITY_HARDWARE,
SCAN_BUTTON_TITLE,
SCAN_RESULT_MODAL_TITLE,
SCAN_RESULT_CLOSE_BUTTON
SCAN_RESULT_CLOSE_BUTTON,
SCANNER_CAMERA_TAP_TO_RESUME
};

View File

@ -10,8 +10,24 @@
//Типы кодов для распознавания: QR и распространённые штрихкоды
const DEFAULT_CODE_TYPES = ['qr', 'code-128', 'code-39', 'ean-13', 'ean-8', 'upc-a', 'upc-e', 'pdf-417', 'aztec', 'data-matrix'];
//Встроенный сканер устройства: имя события от нативного модуля (Android)
const HARDWARE_SCANNER_EVENT_NAME = 'HardwareScannerData';
//Событие нажатия кнопки встроенного сканера (142/141) — для временного скрытия камеры и перехода в режим «ввод от сканера»
const HARDWARE_SCANNER_TRIGGER_EVENT_NAME = 'HardwareScannerTriggerPressed';
//Диапазоны символов, допустимых в данных штрихкодов/QR: ASCII printable + Latin-1
const HARDWARE_SCANNER_CHAR_ASCII_MIN = 32;
const HARDWARE_SCANNER_CHAR_ASCII_MAX = 126;
const HARDWARE_SCANNER_CHAR_LATIN1_MIN = 160;
const HARDWARE_SCANNER_CHAR_LATIN1_MAX = 255;
//Длительность (мс) скрытия камеры после нажатия кнопки встроенного сканера, чтобы прошивка успела включить лазер
const HARDWARE_SCANNER_CAMERA_HIDE_MS = 3500;
//Определение, должен ли отображаться сканер на главном экране
function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInProgress }) {
function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInProgress, isOfflineMessageVisible, isAboutMessageVisible }) {
if (isOfflineMessageVisible) return false;
if (isAboutMessageVisible) return false;
return Boolean(alwaysShowScanner && !hasScanResult && !isStartupCheckInProgress);
}
@ -21,5 +37,12 @@ function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInP
module.exports = {
DEFAULT_CODE_TYPES,
HARDWARE_SCANNER_EVENT_NAME,
HARDWARE_SCANNER_TRIGGER_EVENT_NAME,
HARDWARE_SCANNER_CAMERA_HIDE_MS,
HARDWARE_SCANNER_CHAR_ASCII_MIN,
HARDWARE_SCANNER_CHAR_ASCII_MAX,
HARDWARE_SCANNER_CHAR_LATIN1_MIN,
HARDWARE_SCANNER_CHAR_LATIN1_MAX,
shouldShowScanner
};

View File

@ -44,7 +44,6 @@ class SQLiteDatabase {
this.isInitialized = true;
return this.db;
} catch (error) {
console.error('Ошибка инициализации базы данных:', error);
throw error;
}
}
@ -59,15 +58,8 @@ class SQLiteDatabase {
//Выполняем SQL запросы последовательно
await this.executeQuery(this.sqlQueries.CREATE_TABLE_APP_SETTINGS);
await this.executeQuery(this.sqlQueries.CREATE_TABLE_INSPECTIONS);
await this.executeQuery(this.sqlQueries.CREATE_TABLE_AUTH_SESSION);
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS);
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_CREATED);
} catch (error) {
console.error('Ошибка настройки базы данных:', error);
throw error;
}
}
@ -82,7 +74,6 @@ class SQLiteDatabase {
const result = await this.db.executeAsync(sql, params);
return result;
} catch (error) {
console.error('Ошибка выполнения SQL запроса:', error, 'SQL:', sql, 'Params:', params);
throw error;
}
}
@ -97,7 +88,6 @@ class SQLiteDatabase {
}
return null;
} catch (error) {
console.error('Ошибка получения настройки:', error);
throw error;
}
}
@ -109,7 +99,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.SETTINGS_SET, [key, stringValue]);
return true;
} catch (error) {
console.error('Ошибка сохранения настройки:', error);
throw error;
}
}
@ -120,7 +109,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.SETTINGS_DELETE, [key]);
return true;
} catch (error) {
console.error('Ошибка удаления настройки:', error);
throw error;
}
}
@ -144,7 +132,6 @@ class SQLiteDatabase {
return settings;
} catch (error) {
console.error('Ошибка получения всех настроек:', error);
throw error;
}
}
@ -155,126 +142,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.SETTINGS_CLEAR_ALL, []);
return true;
} catch (error) {
console.error('Ошибка очистки настроек:', error);
throw error;
}
}
//Сохранение осмотра
async saveInspection(inspection) {
try {
const { id, title, status, data } = inspection;
const dataString = data ? JSON.stringify(data) : null;
await this.executeQuery(this.sqlQueries.INSPECTIONS_UPSERT, [id, title, status, id, dataString]);
return true;
} catch (error) {
console.error('Ошибка сохранения осмотра:', error);
throw error;
}
}
//Получение всех осмотров
async getInspections() {
try {
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_ALL, []);
const inspections = [];
if (result.rows && result.rows.length > 0) {
for (let i = 0; i < result.rows.length; i++) {
const row = result.rows.item(i);
const inspection = {
id: row.id,
title: row.title,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at
};
if (row.data) {
try {
inspection.data = JSON.parse(row.data);
} catch {
inspection.data = row.data;
}
}
inspections.push(inspection);
}
}
return inspections;
} catch (error) {
console.error('Ошибка получения осмотров:', error);
throw error;
}
}
//Получение осмотра по ID
async getInspectionById(id) {
try {
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_BY_ID, [id]);
if (result.rows && result.rows.length > 0) {
const row = result.rows.item(0);
const inspection = {
id: row.id,
title: row.title,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at
};
if (row.data) {
try {
inspection.data = JSON.parse(row.data);
} catch {
inspection.data = row.data;
}
}
return inspection;
}
return null;
} catch (error) {
console.error('Ошибка получения осмотра по ID:', error);
throw error;
}
}
//Удаление осмотра
async deleteInspection(id) {
try {
await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE, [id]);
return true;
} catch (error) {
console.error('Ошибка удаления осмотра:', error);
throw error;
}
}
//Удаление всех осмотров
async clearInspections() {
try {
await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE_ALL, []);
return true;
} catch (error) {
console.error('Ошибка очистки осмотров:', error);
throw error;
}
}
//Получение количества осмотров
async getInspectionsCount() {
try {
const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_COUNT, []);
if (result.rows && result.rows.length > 0) {
return result.rows.item(0).count;
}
return 0;
} catch (error) {
console.error('Ошибка получения количества осмотров:', error);
throw error;
}
}
@ -285,7 +152,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.UTILITY_VACUUM, []);
return true;
} catch (error) {
console.error('Ошибка оптимизации базы данных:', error);
throw error;
}
}
@ -316,7 +182,6 @@ class SQLiteDatabase {
]);
return true;
} catch (error) {
console.error('Ошибка сохранения сессии авторизации:', error);
throw error;
}
}
@ -343,7 +208,6 @@ class SQLiteDatabase {
}
return null;
} catch (error) {
console.error('Ошибка получения сессии авторизации:', error);
throw error;
}
}
@ -354,7 +218,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.AUTH_SESSION_CLEAR, []);
return true;
} catch (error) {
console.error('Ошибка очистки сессии авторизации:', error);
throw error;
}
}
@ -365,7 +228,6 @@ class SQLiteDatabase {
const result = await this.executeQuery(this.sqlQueries.UTILITY_CHECK_TABLE, [tableName]);
return result.rows && result.rows.length > 0 && result.rows.item(0).exists === 1;
} catch (error) {
console.error('Ошибка проверки существования таблицы:', error);
throw error;
}
}
@ -376,7 +238,6 @@ class SQLiteDatabase {
await this.executeQuery(this.sqlQueries.UTILITY_DROP_TABLE, [tableName]);
return true;
} catch (error) {
console.error('Ошибка удаления таблицы:', error);
throw error;
}
}
@ -385,13 +246,12 @@ class SQLiteDatabase {
async close() {
try {
if (this.db) {
// В react-native-quick-sqlite нет явного метода close
// База данных закрывается автоматически при уничтожении объекта
//В react-native-quick-sqlite нет явного метода close
//База данных закрывается автоматически при уничтожении объекта
this.db = null;
this.isInitialized = false;
}
} catch (error) {
console.error('Ошибка закрытия базы данных:', error);
this.db = null;
this.isInitialized = false;
throw error;

View File

@ -9,13 +9,8 @@
//Таблицы
const CREATE_TABLE_APP_SETTINGS = require('./settings/create_table_app_settings.sql');
const CREATE_TABLE_INSPECTIONS = require('./inspections/create_table_inspections.sql');
const CREATE_TABLE_AUTH_SESSION = require('./auth/create_table_auth_session.sql');
//Индексы
const CREATE_INDEX_INSPECTIONS_STATUS = require('./inspections/create_index_inspections_status.sql');
const CREATE_INDEX_INSPECTIONS_CREATED = require('./inspections/create_index_inspections_created.sql');
//Настройки
const SETTINGS_GET = require('./settings/get_setting.sql');
const SETTINGS_SET = require('./settings/set_setting.sql');
@ -23,15 +18,6 @@ const SETTINGS_DELETE = require('./settings/delete_setting.sql');
const SETTINGS_CLEAR_ALL = require('./settings/clear_all_settings.sql');
const SETTINGS_GET_ALL = require('./settings/get_all_settings.sql');
//Осмотры
const INSPECTIONS_INSERT = require('./inspections/insert_inspection.sql');
const INSPECTIONS_UPSERT = require('./inspections/upsert_inspection.sql');
const INSPECTIONS_GET_ALL = require('./inspections/get_all_inspections.sql');
const INSPECTIONS_GET_BY_ID = require('./inspections/get_inspection_by_id.sql');
const INSPECTIONS_DELETE = require('./inspections/delete_inspection.sql');
const INSPECTIONS_DELETE_ALL = require('./inspections/delete_all_inspections.sql');
const INSPECTIONS_COUNT = require('./inspections/count_inspections.sql');
//Авторизация
const AUTH_SESSION_SET = require('./auth/set_auth_session.sql');
const AUTH_SESSION_GET = require('./auth/get_auth_session.sql');
@ -49,22 +35,12 @@ const UTILITY_VACUUM = require('./utility/vacuum.sql');
//Сбор всех SQL запросов в один объект
const SQLQueries = {
CREATE_TABLE_APP_SETTINGS,
CREATE_TABLE_INSPECTIONS,
CREATE_TABLE_AUTH_SESSION,
CREATE_INDEX_INSPECTIONS_STATUS,
CREATE_INDEX_INSPECTIONS_CREATED,
SETTINGS_GET,
SETTINGS_SET,
SETTINGS_DELETE,
SETTINGS_CLEAR_ALL,
SETTINGS_GET_ALL,
INSPECTIONS_INSERT,
INSPECTIONS_UPSERT,
INSPECTIONS_GET_ALL,
INSPECTIONS_GET_BY_ID,
INSPECTIONS_DELETE,
INSPECTIONS_DELETE_ALL,
INSPECTIONS_COUNT,
AUTH_SESSION_SET,
AUTH_SESSION_GET,
AUTH_SESSION_CLEAR,

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: подсчет количества осмотров
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_COUNT = `
-- Подсчет количества осмотров
SELECT COUNT(*) as count FROM inspections;
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_COUNT;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: создание индекса по дате создания
*/
//-----------
//Тело модуля
//-----------
const CREATE_INDEX_INSPECTIONS_CREATED = `
-- Индекс для сортировки по дате создания
CREATE INDEX IF NOT EXISTS idx_inspections_created ON inspections(created_at DESC);
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = CREATE_INDEX_INSPECTIONS_CREATED;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: создание индекса по статусу
*/
//-----------
//Тело модуля
//-----------
const CREATE_INDEX_INSPECTIONS_STATUS = `
-- Индекс для быстрого поиска осмотров по статусу
CREATE INDEX IF NOT EXISTS idx_inspections_status ON inspections(status);
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = CREATE_INDEX_INSPECTIONS_STATUS;

View File

@ -1,26 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: создание таблицы осмотров
*/
//-----------
//Тело модуля
//-----------
const CREATE_TABLE_INSPECTIONS = `
-- Таблица для предрейсовых осмотров
CREATE TABLE IF NOT EXISTS inspections (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
data TEXT
);
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = CREATE_TABLE_INSPECTIONS;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: удаление всех осмотров
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_DELETE_ALL = `
-- Удаление всех осмотров
DELETE FROM inspections;
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_DELETE_ALL;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: удаление осмотра
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_DELETE = `
-- Удаление осмотра по ID
DELETE FROM inspections WHERE id = ?;
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_DELETE;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: получение всех осмотров
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_GET_ALL = `
-- Получение всех осмотров
SELECT * FROM inspections ORDER BY created_at DESC;
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_GET_ALL;

View File

@ -1,19 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: получение осмотра по ID
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_GET_BY_ID = `
-- Получение осмотра по ID
SELECT * FROM inspections WHERE id = ?;
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_GET_BY_ID;

View File

@ -1,20 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: вставка нового осмотра
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_INSERT = `
-- Вставка нового осмотра
INSERT INTO inspections (id, title, status, created_at, data)
VALUES (?, ?, ?, ?, ?);
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_INSERT;

View File

@ -1,20 +0,0 @@
/*
Предрейсовые осмотры - мобильное приложение
SQL запрос: вставка или обновление осмотра
*/
//-----------
//Тело модуля
//-----------
const INSPECTIONS_UPSERT = `
-- Вставка или обновление осмотра
INSERT OR REPLACE INTO inspections (id, title, status, created_at, updated_at, data)
VALUES (?, ?, ?, COALESCE((SELECT created_at FROM inspections WHERE id = ?), CURRENT_TIMESTAMP), CURRENT_TIMESTAMP, ?);
`;
//----------------
//Интерфейс модуля
//----------------
module.exports = INSPECTIONS_UPSERT;

View File

@ -52,7 +52,6 @@ function useAppMode() {
}
//При отсутствии сохранённого режима остаётся NOT_CONNECTED
} catch (error) {
console.error('Ошибка загрузки режима:', error);
} finally {
setIsInitialized(true);
loadingRef.current = false;
@ -71,9 +70,7 @@ function useAppMode() {
const saveMode = async () => {
try {
await setSetting(STORAGE_KEY, mode);
} catch (error) {
console.error('Ошибка сохранения режима:', error);
}
} catch (error) {}
};
saveMode();

View File

@ -9,6 +9,7 @@
const React = require('react'); //React и хуки
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
//---------
//Константы
@ -60,9 +61,7 @@ function useAppServer() {
let baseURL = '';
try {
baseURL = (await getSetting('app_server_url')) || '';
} catch (e) {
console.warn('Не удалось прочитать настройки сервера:', e);
}
} catch (_e) {}
if (!baseURL) {
return makeRespErr({
@ -77,6 +76,14 @@ function useAppServer() {
const abortController = new AbortController();
abortControllerRef.current = abortController;
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
let timeoutId = null;
if (timeoutMs > 0) {
timeoutId = setTimeout(() => {
abortController.abort();
}, timeoutMs);
}
let response = null;
let responseJSON = null;
@ -97,6 +104,9 @@ function useAppServer() {
message: `${ERR_NETWORK}: ${e.message || 'неопределённая ошибка'}`
});
} finally {
if (timeoutId != null) {
clearTimeout(timeoutId);
}
abortControllerRef.current = null;
}

View File

@ -9,10 +9,11 @@
const React = require('react'); //React и хуки
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации
const { ACTION_CODES, RESPONSE_STATES, ERROR_MESSAGES } = require('../config/authApi'); //API авторизации
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage'); //Шифрование
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId'); //Идентификатор устройства
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
//-----------
//Тело модуля
@ -79,41 +80,55 @@ function useAuth() {
}, [getSetting, setSetting]);
//Выполнение запроса к серверу
const executeRequest = React.useCallback(async (serverUrl, payload) => {
//Отменяем предыдущий запрос если есть
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
try {
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
const executeRequest = React.useCallback(
async (serverUrl, payload) => {
//Отменяем предыдущий запрос если есть
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
const data = await response.json();
return data;
} catch (fetchError) {
if (fetchError.name === 'AbortError') {
return null;
}
const abortController = new AbortController();
abortControllerRef.current = abortController;
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
} finally {
abortControllerRef.current = null;
}
}, []);
let timeoutId = null;
try {
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
if (timeoutMs > 0) {
timeoutId = setTimeout(() => {
abortController.abort();
}, timeoutMs);
}
const response = await fetch(serverUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload),
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
}
const data = await response.json();
return data;
} catch (fetchError) {
if (fetchError.name === 'AbortError') {
return null;
}
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
} finally {
if (timeoutId != null) {
clearTimeout(timeoutId);
}
abortControllerRef.current = null;
}
},
[getSetting]
);
//Сохранение credentials после успешной аутентификации
const saveCredentials = React.useCallback(
@ -131,7 +146,6 @@ function useAuth() {
return true;
} catch (saveError) {
console.error('Ошибка сохранения credentials:', saveError);
return false;
}
},
@ -182,7 +196,6 @@ function useAuth() {
savePasswordEnabled: true
};
} catch (loadError) {
console.error('Ошибка загрузки credentials:', loadError);
return null;
}
}, [getSetting, getDeviceId, getSecretKey]);
@ -195,7 +208,6 @@ function useAuth() {
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
return true;
} catch (clearError) {
console.error('Ошибка очистки credentials:', clearError);
return false;
}
}, [setSetting]);
@ -229,8 +241,8 @@ function useAuth() {
requestPayload.XREQUEST.XPAYLOAD.SCOMPANY = company;
}
//Добавляем таймаут (используем значение по умолчанию если не указан)
const timeoutValue = timeout && timeout > 0 ? timeout : DEFAULT_IDLE_TIMEOUT;
//Таймаут сессии на сервере (минуты)
const timeoutValue = timeout != null && Number(timeout) > 0 ? Number(timeout) : 0;
requestPayload.XREQUEST.XPAYLOAD.NTIMEOUT = timeoutValue;
//Выполняем запрос
@ -405,16 +417,18 @@ function useAuth() {
//Выход из системы
const logout = React.useCallback(
async (options = {}) => {
const { skipServerRequest = false } = options;
const { skipServerRequest = false, keepSessionLocally = false } = options;
setIsLoading(true);
setIsLogoutInProgress(true);
if (!keepSessionLocally) {
setIsLoading(true);
setIsLogoutInProgress(true);
}
setError(null);
try {
const currentSession = session || (await getAuthSession());
//Запрос на сервер только при онлайн и если не отключено опцией
//Запрос на сервер при наличии сессии и если не отключено опцией
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
const requestPayload = {
XREQUEST: {
@ -429,14 +443,14 @@ function useAuth() {
try {
await executeRequest(currentSession.serverUrl, requestPayload);
} catch (logoutError) {
console.warn('Ошибка при выходе из системы:', logoutError);
}
} catch (_logoutError) {}
}
await clearAuthSession();
setSession(null);
setIsAuthenticated(false);
if (!keepSessionLocally) {
await clearAuthSession();
setSession(null);
setIsAuthenticated(false);
}
return { success: true };
} catch (logoutError) {
@ -444,8 +458,10 @@ function useAuth() {
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setIsLoading(false);
setIsLogoutInProgress(false);
if (!keepSessionLocally) {
setIsLoading(false);
setIsLogoutInProgress(false);
}
}
},
[session, getAuthSession, executeRequest, clearAuthSession]
@ -580,8 +596,7 @@ function useAuth() {
setIsAuthenticated(true);
sessionRestoredFromStorageRef.current = true;
}
} catch (initError) {
console.error('Ошибка инициализации авторизации:', initError);
} catch (_initError) {
} finally {
setIsInitialized(true);
}

View File

@ -0,0 +1,121 @@
/*
Предрейсовые осмотры - мобильное приложение
Хук отслеживания простоя
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react'); //React и хуки
const { AppState } = require('react-native'); //Состояние приложения (active/background)
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 { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
const { IDLE_TIMEOUT_OFFLINE_MESSAGE, OFFLINE_MODE_TITLE } = require('../config/messages'); //Сообщения
//---------
//Константы
//---------
const IDLE_CHECK_INTERVAL_MS = 15000; //Проверка каждые 15 секунд
//-----------
//Тело модуля
//-----------
//Хук отслеживания простоя: при превышении лимита — запрос logout на сервер, переход в офлайн и сообщение
function useIdleTimeout() {
const { getSetting } = useAppLocalDbContext();
const { isAuthenticated, logout } = useAppAuthContext();
const { APP_MODE, mode, setOffline } = useAppModeContext();
const { showInfo } = useAppMessagingContext();
const lastActivityAtRef = React.useRef(Date.now());
const intervalIdRef = React.useRef(null);
const appStateRef = React.useRef(AppState.currentState);
//Сброс таймера простоя при любой активности
const reportActivity = React.useCallback(() => {
lastActivityAtRef.current = Date.now();
}, []);
//Проверка простоя
const checkIdleAndLogout = React.useCallback(async () => {
if (!isAuthenticated || appStateRef.current !== 'active' || mode === APP_MODE.OFFLINE) {
return;
}
let idleMinutes = 0;
try {
const raw = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
const trimmed = raw != null ? String(raw).trim() : '';
if (trimmed === '') {
return;
}
idleMinutes = parseInt(trimmed, 10);
if (isNaN(idleMinutes) || idleMinutes <= 0) {
return;
}
} catch (err) {
return;
}
const idleLimitMs = idleMinutes * 60 * 1000;
const now = Date.now();
if (now - lastActivityAtRef.current >= idleLimitMs) {
try {
await logout({ skipServerRequest: false, keepSessionLocally: true });
setOffline();
showInfo(IDLE_TIMEOUT_OFFLINE_MESSAGE, { title: OFFLINE_MODE_TITLE });
} catch (logoutErr) {}
}
}, [isAuthenticated, getSetting, logout, mode, APP_MODE.OFFLINE, setOffline, showInfo]);
//Запуск периодической проверки при авторизации; сброс времени активности при входе
React.useEffect(() => {
if (!isAuthenticated) {
if (intervalIdRef.current != null) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
return;
}
lastActivityAtRef.current = Date.now();
const intervalId = setInterval(checkIdleAndLogout, IDLE_CHECK_INTERVAL_MS);
intervalIdRef.current = intervalId;
return function cleanup() {
if (intervalIdRef.current != null) {
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
}, [isAuthenticated, checkIdleAndLogout]);
//Учёт перехода приложения в фон/на передний план (не считать простой в фоне)
React.useEffect(() => {
const subscription = AppState.addEventListener('change', nextAppState => {
appStateRef.current = nextAppState;
if (nextAppState === 'active') {
lastActivityAtRef.current = Date.now();
}
});
return function cleanup() {
subscription.remove();
};
}, []);
return { reportActivity };
}
//----------------
//Интерфейс модуля
//----------------
module.exports = useIdleTimeout;

View File

@ -17,7 +17,6 @@ const SQLiteDatabase = require('../database/SQLiteDatabase'); //Модуль SQL
function useLocalDb() {
//Состояние хука
const [isDbReady, setIsDbReady] = React.useState(false);
const [inspections, setInspections] = React.useState([]);
const [dbError, setDbError] = React.useState(null);
//Отслеживания статуса инициализации
@ -33,12 +32,6 @@ function useLocalDb() {
if (initializingRef.current || SQLiteDatabase.isInitialized) {
if (mounted && SQLiteDatabase.isInitialized) {
setIsDbReady(true);
try {
const loadedInspections = await SQLiteDatabase.getInspections();
setInspections(loadedInspections);
} catch (loadError) {
console.error('Ошибка загрузки осмотров при переинициализации:', loadError);
}
}
return;
}
@ -49,12 +42,9 @@ function useLocalDb() {
await SQLiteDatabase.initialize();
if (mounted) {
setIsDbReady(true);
const loadedInspections = await SQLiteDatabase.getInspections();
setInspections(loadedInspections);
setDbError(null);
}
} catch (initError) {
console.error('Ошибка инициализации базы данных:', initError);
} catch (_initError) {
if (mounted) {
setIsDbReady(false);
setDbError('Не удалось инициализировать базу данных');
@ -71,48 +61,6 @@ function useLocalDb() {
};
}, []);
//Загрузка списка осмотров
const loadInspections = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return [];
}
try {
const loadedInspections = await SQLiteDatabase.getInspections();
setInspections(loadedInspections);
setDbError(null);
return loadedInspections;
} catch (loadError) {
console.error('Ошибка загрузки осмотров:', loadError);
setDbError('Не удалось загрузить осмотры');
return [];
}
}, [isDbReady]);
//Сохранение осмотра
const saveInspection = React.useCallback(
async inspection => {
if (!isDbReady) {
console.warn('База данных не готова');
return inspection;
}
try {
await SQLiteDatabase.saveInspection(inspection);
const updatedInspections = await SQLiteDatabase.getInspections();
setInspections(updatedInspections);
setDbError(null);
return inspection;
} catch (saveError) {
console.error('Ошибка сохранения осмотра:', saveError);
setDbError('Не удалось сохранить осмотр');
return inspection;
}
},
[isDbReady]
);
//Получение настройки
const getSetting = React.useCallback(
async key => {
@ -122,8 +70,7 @@ function useLocalDb() {
try {
return await SQLiteDatabase.getSetting(key);
} catch (getSettingError) {
console.error('Ошибка получения настройки:', getSettingError);
} catch (_getSettingError) {
return null;
}
},
@ -133,15 +80,11 @@ function useLocalDb() {
//Сохранение настройки
const setSetting = React.useCallback(
async (key, value) => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.setSetting(key, value);
} catch (setSettingError) {
console.error('Ошибка сохранения настройки:', setSettingError);
} catch (_setSettingError) {
return false;
}
},
@ -151,15 +94,11 @@ function useLocalDb() {
//Удаление настройки
const deleteSetting = React.useCallback(
async key => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.deleteSetting(key);
} catch (deleteSettingError) {
console.error('Ошибка удаления настройки:', deleteSettingError);
} catch (_deleteSettingError) {
return false;
}
},
@ -168,66 +107,33 @@ function useLocalDb() {
//Получение всех настроек
const getAllSettings = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return {};
}
if (!isDbReady) return {};
try {
return await SQLiteDatabase.getAllSettings();
} catch (getAllSettingsError) {
console.error('Ошибка получения всех настроек:', getAllSettingsError);
} catch (_getAllSettingsError) {
return {};
}
}, [isDbReady]);
//Очистка всех настроек
const clearSettings = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.clearSettings();
} catch (clearSettingsError) {
console.error('Ошибка очистки настроек:', clearSettingsError);
return false;
}
}, [isDbReady]);
//Очистка всех осмотров
const clearInspections = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
try {
const result = await SQLiteDatabase.clearInspections();
if (result) {
setInspections([]);
setDbError(null);
}
return result;
} catch (clearInspectionsError) {
console.error('Ошибка очистки осмотров:', clearInspectionsError);
setDbError('Не удалось очистить осмотры');
} catch (_clearSettingsError) {
return false;
}
}, [isDbReady]);
//Оптимизация базы данных
const vacuum = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.vacuum();
} catch (vacuumError) {
console.error('Ошибка оптимизации базы данных:', vacuumError);
} catch (_vacuumError) {
return false;
}
}, [isDbReady]);
@ -235,15 +141,11 @@ function useLocalDb() {
//Проверка существования таблицы
const checkTableExists = React.useCallback(
async tableName => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.checkTableExists(tableName);
} catch (checkTableError) {
console.error('Ошибка проверки существования таблицы:', checkTableError);
} catch (_checkTableError) {
return false;
}
},
@ -253,15 +155,11 @@ function useLocalDb() {
//Сохранение сессии авторизации
const setAuthSession = React.useCallback(
async session => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.setAuthSession(session);
} catch (setAuthSessionError) {
console.error('Ошибка сохранения сессии авторизации:', setAuthSessionError);
} catch (_setAuthSessionError) {
return false;
}
},
@ -270,46 +168,34 @@ function useLocalDb() {
//Получение сессии авторизации
const getAuthSession = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return null;
}
if (!isDbReady) return null;
try {
return await SQLiteDatabase.getAuthSession();
} catch (getAuthSessionError) {
console.error('Ошибка получения сессии авторизации:', getAuthSessionError);
} catch (_getAuthSessionError) {
return null;
}
}, [isDbReady]);
//Очистка сессии авторизации
const clearAuthSession = React.useCallback(async () => {
if (!isDbReady) {
console.warn('База данных не готова');
return false;
}
if (!isDbReady) return false;
try {
return await SQLiteDatabase.clearAuthSession();
} catch (clearAuthSessionError) {
console.error('Ошибка очистки сессии авторизации:', clearAuthSessionError);
} catch (_clearAuthSessionError) {
return false;
}
}, [isDbReady]);
return {
isDbReady,
inspections,
error: dbError,
loadInspections,
saveInspection,
getSetting,
setSetting,
deleteSetting,
getAllSettings,
clearSettings,
clearInspections,
vacuum,
checkTableExists,
setAuthSession,

View File

@ -0,0 +1,76 @@
/*
Предрейсовые осмотры - мобильное приложение
Универсальный хук обработки данных встроенного сканера для экранов с полями ввода
*/
//---------------------
//Подключение библиотек
//---------------------
const React = require('react'); //React и хуки
const { resolveScanTarget, getNextInOrder } = require('../utils/scanInputTargetResolver'); //Резолвер цели для сканера
//-----------
//Тело модуля
//-----------
//Хук формирования обработчика данных от встроенного сканера
function useScanInputHandler(options) {
const { allowedFieldIds, focusedFieldRef, expectedNextTargetRef, setFieldValue, focusField, parseSegments, onSubmitForm } = options;
const handleHardwareScan = React.useCallback(
barcode => {
const segments = parseSegments(barcode);
if (segments.length === 0) {
return;
}
const allowed = Array.isArray(allowedFieldIds) ? allowedFieldIds : [];
if (allowed.length === 0) {
return;
}
const focused = focusedFieldRef.current;
let targetField = resolveScanTarget(focused, allowed);
if (targetField == null) {
return;
}
let lastTargetField = targetField;
for (let i = 0; i < segments.length; i++) {
const value = segments[i];
setFieldValue(targetField, value);
lastTargetField = targetField;
const nextField = getNextInOrder(targetField, allowed);
if (nextField && i < segments.length - 1) {
expectedNextTargetRef.current = nextField;
focusedFieldRef.current = nextField;
targetField = nextField;
} else if (!nextField) {
focusedFieldRef.current = null;
expectedNextTargetRef.current = null;
if (typeof onSubmitForm === 'function') {
onSubmitForm();
}
return;
} else {
expectedNextTargetRef.current = nextField;
focusedFieldRef.current = nextField;
focusField(nextField);
return;
}
}
focusedFieldRef.current = lastTargetField;
focusField(lastTargetField);
},
[allowedFieldIds, focusedFieldRef, expectedNextTargetRef, setFieldValue, focusField, parseSegments, onSubmitForm]
);
return handleHardwareScan;
}
//----------------
//Интерфейс модуля
//----------------
module.exports = useScanInputHandler;

View File

@ -23,12 +23,17 @@ const OrganizationSelectDialog = require('../components/auth/OrganizationSelectD
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
const { isServerUrlFieldVisible } = require('../utils/loginFormUtils'); //Утилиты формы входа
const { parseScannedSegmentsForAuth } = require('../utils/authScannerUtils'); //Разбор данных сканера по Enter для экрана входа
const { getAllowedAuthFields } = require('../utils/authFormFieldsOrder'); //Порядок и доступность полей формы для сканера
const useScanInputHandler = require('../hooks/useScanInputHandler'); //Универсальный обработчик данных сканера для полей ввода
const { normalizeServerUrl, validateServerUrl } = require('../utils/validation'); //Валидация
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGOUT, MENU_ITEM_ID_DIVIDER } = require('../config/menuItemIds'); //ID пунктов меню
const { getAuthFormStore, setAuthFormStore, clearAuthFormStore } = require('../utils/authFormStore'); //Хранилище формы входа
const {
APP_ABOUT_TITLE,
@ -36,14 +41,22 @@ const {
ORGANIZATION_SELECT_DIALOG_TITLE,
MENU_ITEM_SETTINGS,
MENU_ITEM_ABOUT,
MENU_ITEM_LOGOUT,
AUTH_SCREEN_TITLE,
AUTH_BUTTON_LOGIN,
AUTH_BUTTON_LOADING,
AUTH_BUTTON_LOGOUT,
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
AUTH_SERVER_CHANGE_CANCEL_BUTTON
AUTH_SERVER_CHANGE_CANCEL_BUTTON,
LOGOUT_CONFIRM_TITLE,
LOGOUT_CONFIRM_MESSAGE
} = require('../config/messages'); //Сообщения
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
const { createLogoutHandler } = require('../utils/logoutFlow'); //Универсальный поток выхода
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
const { noopCatch } = require('../utils/noop'); //Пустой обработчик для .catch()
const styles = require('../styles/screens/AuthScreen.styles'); //Стили экрана
//-----------
@ -53,12 +66,13 @@ const styles = require('../styles/screens/AuthScreen.styles'); //Стили эк
//Экран аутентификации
function AuthScreen() {
const { showError, showInfo } = useAppMessagingContext();
const { APP_MODE, mode, setOnline } = useAppModeContext();
const { navigate, goBack, canGoBack, reset, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
const { getSetting, isDbReady, clearInspections } = useAppLocalDbContext();
const { APP_MODE, mode, setOnline, setNotConnected } = useAppModeContext();
const { navigate, goBack, canGoBack, reset, setInitialScreen, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
const { getSetting, isDbReady } = useAppLocalDbContext();
const {
session,
login,
logout,
selectCompany,
isLoading,
getSavedCredentials,
@ -115,6 +129,42 @@ function AuthScreen() {
const loginInputRef = React.useRef(null);
const passwordInputRef = React.useRef(null);
//Поле ввода, на котором сейчас фокус (null — фокус сброшен)
const focusedFieldRef = React.useRef(null);
//После программного перехода фокуса сканером — следующее поле (для случая сброса фокуса устройством)
const expectedNextTargetRef = React.useRef(null);
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
//Первый видимый элемент ввода на экране аутентификации (для подстановки при отсутствии фокуса)
const firstVisibleAuthField = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings) ? 'server' : 'login';
//Список доступных полей для сканера (видимые и редактируемые)
const allowedAuthFieldIds = React.useMemo(
() =>
getAllowedAuthFields({
serverVisible: firstVisibleAuthField === 'server',
serverEditable: !(isLoading || isFromMenu),
loginEditable: !(isLoading || isFromMenu),
passwordEditable: !isLoading
}),
[firstVisibleAuthField, isLoading, isFromMenu]
);
//Обработчики фокуса полей
const handleServerFocus = React.useCallback(() => {
focusedFieldRef.current = 'server';
}, []);
const handleLoginFocus = React.useCallback(() => {
focusedFieldRef.current = 'login';
}, []);
const handlePasswordFocus = React.useCallback(() => {
focusedFieldRef.current = 'password';
}, []);
const handleFieldBlur = React.useCallback(() => {
focusedFieldRef.current = null;
}, []);
//Актуализация ref и store при изменении полей; при размонтировании — сохранение в store
React.useEffect(() => {
const next = { serverUrl, username, password, savePassword, showPassword };
@ -183,7 +233,7 @@ function AuthScreen() {
setUsername(val.trim());
}
})
.catch(() => {});
.catch(noopCatch);
return () => {
cancelled = true;
};
@ -240,7 +290,6 @@ function AuthScreen() {
initialLoadRef.current = true;
} catch (loadError) {
console.error('Ошибка загрузки credentials из меню:', loadError);
setUsername('');
setPassword('');
setSavePassword(false);
@ -264,9 +313,7 @@ function AuthScreen() {
if (savedLogin && typeof savedLogin === 'string') {
setUsername(savedLogin.trim());
}
} catch (e) {
console.warn('Резервная загрузка логина:', e);
}
} catch (_e) {}
};
loadSavedLoginOnly();
@ -288,9 +335,7 @@ function AuthScreen() {
}
setSavedServerUrlFromSettings(trimmedUrl);
setHideServerUrl(savedHide === 'true' || savedHide === true);
} catch (e) {
console.warn('Загрузка адреса сервера и настройки видимости:', e);
}
} catch (_e) {}
};
loadServerAndVisibility();
@ -338,8 +383,7 @@ function AuthScreen() {
setPassword('');
setSavePassword(false);
}
} catch (loadError) {
console.error('Ошибка загрузки настроек авторизации:', loadError);
} catch (_loadError) {
} finally {
setIsSettingsLoaded(true);
}
@ -372,45 +416,77 @@ function AuthScreen() {
}, [hideServerUrl, serverUrl, username, password, showError]);
//Выполнение входа
const performLogin = React.useCallback(async () => {
let idleTimeout = null;
try {
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
} catch (settingError) {
console.warn('Ошибка получения таймаута:', settingError);
}
const performLogin = React.useCallback(
async formOverride => {
const data = formOverride != null ? formOverride : { serverUrl, username, password, savePassword };
const s = (data.serverUrl != null ? String(data.serverUrl) : '').trim();
const u = (data.username != null ? String(data.username) : '').trim();
const p = (data.password != null ? String(data.password) : '').trim();
const save = Boolean(data.savePassword);
const result = await login({
serverUrl: serverUrl.trim(),
user: username.trim(),
password: password.trim(),
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
savePassword
});
let idleTimeout = null;
try {
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
} catch (_settingError) {}
if (!result.success) {
showError(result.error || 'Ошибка входа');
return;
}
if (result.needSelectCompany) {
setOrganizations(result.organizations);
setPendingLoginData({
serverUrl: result.serverUrl,
sessionId: result.sessionId,
user: result.user,
savePassword: result.savePassword,
loginCredentials: result.loginCredentials
const result = await login({
serverUrl: s,
user: u,
password: p,
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
savePassword: save
});
setShowOrgDialog(true);
if (!result.success) {
showError(result.error || 'Ошибка входа');
return;
}
if (result.needSelectCompany) {
setOrganizations(result.organizations);
setPendingLoginData({
serverUrl: result.serverUrl,
sessionId: result.sessionId,
user: result.user,
savePassword: result.savePassword,
loginCredentials: result.loginCredentials
});
setShowOrgDialog(true);
return;
}
setOnline();
clearAuthFormData();
clearAuthFormStore();
reset();
},
[login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, reset]
);
//Вход с данными из ref (после подстановки пароля встроенным сканером, чтобы не терять значение до применения setState)
const performLoginWithFormRef = React.useCallback(() => {
const form = formDataRef.current;
if (!form) return;
const s = (form.serverUrl != null ? String(form.serverUrl) : '').trim();
const u = (form.username != null ? String(form.username) : '').trim();
const p = (form.password != null ? String(form.password) : '').trim();
if (!hideServerUrl || !form.serverUrl) {
const urlResult = validateServerUrl(s, { emptyMessage: 'Укажите адрес сервера' });
if (urlResult !== true) {
showError(urlResult);
return;
}
}
if (!u) {
showError('Укажите логин');
return;
}
setOnline();
clearAuthFormData();
clearAuthFormStore();
reset();
}, [login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, reset]);
if (!p) {
showError('Укажите пароль');
return;
}
performLogin(form);
}, [hideServerUrl, showError, performLogin]);
//Обработчик входа (очистка локальных данных только при смене сервера относительно последнего успешного подключения)
const handleLogin = React.useCallback(async () => {
@ -440,7 +516,6 @@ function AuthScreen() {
title: AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
onPress: async () => {
await clearAuthSession();
await clearInspections();
await performLogin();
}
}
@ -449,7 +524,7 @@ function AuthScreen() {
} else {
await performLogin();
}
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, clearInspections, performLogin]);
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, performLogin]);
//Обработчик выбора организации
const handleSelectOrganization = React.useCallback(
@ -536,6 +611,60 @@ function AuthScreen() {
handleLogin();
}, [handleLogin]);
//Подстановка значения в поле формы по идентификатору (state, ref, store)
const setAuthFieldValue = React.useCallback((fieldId, value) => {
const v = value != null ? String(value) : '';
if (fieldId === 'server') {
formDataRef.current.serverUrl = v;
setAuthFormStore({ serverUrl: v });
setServerUrl(v);
} else if (fieldId === 'login') {
formDataRef.current.username = v;
setAuthFormStore({ username: v });
setUsername(v);
} else if (fieldId === 'password') {
formDataRef.current.password = v;
setAuthFormStore({ password: v });
setPassword(v);
}
}, []);
//Перенос фокуса на поле формы по идентификатору
const focusAuthField = React.useCallback(fieldId => {
if (fieldId === 'server' && serverInputRef.current) {
serverInputRef.current.focus();
} else if (fieldId === 'login' && loginInputRef.current) {
loginInputRef.current.focus();
} else if (fieldId === 'password' && passwordInputRef.current) {
passwordInputRef.current.focus();
}
}, []);
//Обработчик встроенного сканера: подстановка в фокусное или первое доступное поле; при Enter — следующее поле или «Войти»
const handleHardwareScan = useScanInputHandler({
allowedFieldIds: allowedAuthFieldIds,
focusedFieldRef,
expectedNextTargetRef,
setFieldValue: setAuthFieldValue,
focusField: focusAuthField,
parseSegments: parseScannedSegmentsForAuth,
onSubmitForm: performLoginWithFormRef
});
const authScanHandlerRef = React.useRef(handleHardwareScan);
authScanHandlerRef.current = handleHardwareScan;
const stableAuthScanHandler = React.useCallback(barcode => {
if (typeof authScanHandlerRef.current === 'function') authScanHandlerRef.current(barcode);
}, []);
//Регистрация обработчика встроенного сканера на экране AUTH
React.useEffect(() => {
registerHandler(scannerScreens.AUTH, stableAuthScanHandler);
return function cleanup() {
unregisterHandler(scannerScreens.AUTH);
};
}, [registerHandler, unregisterHandler, scannerScreens.AUTH, stableAuthScanHandler]);
//Обработчик открытия меню
const handleMenuOpen = React.useCallback(() => {
setMenuVisible(true);
@ -564,24 +693,67 @@ function AuthScreen() {
});
}, [showInfo, mode, serverUrl, isDbReady]);
//Обработчик выхода с экрана аутентификации: сброс стека, нельзя вернуться назад
const logoutHandlerFromAuth = React.useMemo(() => {
if (!isFromMenu) {
return null;
}
return createLogoutHandler({
logout,
mode,
setNotConnected,
setInitialScreen,
SCREENS,
showError,
showInfo,
logoutConfirmTitle: LOGOUT_CONFIRM_TITLE,
logoutConfirmMessage: LOGOUT_CONFIRM_MESSAGE,
logoutButtonTitle: AUTH_BUTTON_LOGOUT,
getConfirmButtonOptions,
dialogButtonTypeError: DIALOG_BUTTON_TYPE.ERROR,
dialogCancelButton: DIALOG_CANCEL_BUTTON
});
}, [isFromMenu, logout, mode, setNotConnected, setInitialScreen, SCREENS, showError, showInfo]);
const handleLogoutFromAuth = React.useCallback(() => {
if (logoutHandlerFromAuth) {
logoutHandlerFromAuth.handleLogout();
}
}, [logoutHandlerFromAuth]);
//Пункты бокового меню
const menuItems = React.useMemo(() => {
return [
const items = [
{
id: 'settings',
id: MENU_ITEM_ID_SETTINGS,
title: MENU_ITEM_SETTINGS,
onPress: handleOpenSettings
},
{
id: 'about',
id: MENU_ITEM_ID_ABOUT,
title: MENU_ITEM_ABOUT,
onPress: handleShowAbout
}
];
}, [handleOpenSettings, handleShowAbout]);
//Поле сервера показываем, если настройка выключена или в настройках ещё не сохранён адрес
const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings);
if (isFromMenu) {
items.push({
id: MENU_ITEM_ID_DIVIDER,
type: 'divider'
});
items.push({
id: MENU_ITEM_ID_LOGOUT,
title: MENU_ITEM_LOGOUT,
onPress: handleLogoutFromAuth,
textStyle: { color: APP_COLORS.error },
iconColor: APP_COLORS.error
});
}
return items;
}, [handleOpenSettings, handleShowAbout, handleLogoutFromAuth, isFromMenu]);
const shouldShowServerUrl = firstVisibleAuthField === 'server';
return (
<AdaptiveView padding={false}>
@ -622,6 +794,8 @@ function AuthScreen() {
blurOnSubmit={false}
returnKeyType="next"
onSubmitEditing={handleServerSubmitEditing}
onFocus={handleServerFocus}
onBlur={handleFieldBlur}
/>
) : null}
@ -638,6 +812,8 @@ function AuthScreen() {
blurOnSubmit={false}
returnKeyType="next"
onSubmitEditing={handleLoginSubmitEditing}
onFocus={handleLoginFocus}
onBlur={handleFieldBlur}
/>
<PasswordInput
@ -653,6 +829,8 @@ function AuthScreen() {
blurOnSubmit={true}
returnKeyType="done"
onSubmitEditing={handlePasswordSubmitEditing}
onFocus={handlePasswordFocus}
onBlur={handleFieldBlur}
/>
<View style={styles.switchContainer}>
@ -666,6 +844,16 @@ function AuthScreen() {
style={styles.loginButton}
textStyle={styles.loginButtonText}
/>
{isFromMenu ? (
<AppButton
title={AUTH_BUTTON_LOGOUT}
onPress={handleLogoutFromAuth}
disabled={isLoading}
style={styles.logoutButton}
textStyle={styles.logoutButtonText}
/>
) : null}
</View>
</ScrollView>
</KeyboardAvoidingView>

View File

@ -12,6 +12,7 @@ const { View } = require('react-native'); //Базовые компоненты
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
@ -20,10 +21,25 @@ 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 { APP_ABOUT_TITLE, SIDE_MENU_TITLE, MENU_ITEM_SETTINGS, MENU_ITEM_ABOUT, MENU_ITEM_LOGIN, MENU_ITEM_LOGOUT } = require('../config/messages'); //Сообщения
const {
APP_ABOUT_TITLE,
SIDE_MENU_TITLE,
MENU_ITEM_SETTINGS,
MENU_ITEM_ABOUT,
MENU_ITEM_LOGIN,
MENU_ITEM_LOGOUT,
OFFLINE_MODE_TITLE,
LOGOUT_CONFIRM_TITLE,
LOGOUT_CONFIRM_MESSAGE,
AUTH_BUTTON_LOGOUT
} = require('../config/messages'); //Сообщения
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
const { createLogoutHandler } = require('../utils/logoutFlow'); //Универсальный поток выхода
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
const { shouldShowScanner } = require('../config/scannerConfig'); //Логика видимости сканера
const { MENU_ITEM_ID_SETTINGS, MENU_ITEM_ID_ABOUT, MENU_ITEM_ID_LOGIN, MENU_ITEM_ID_LOGOUT, MENU_ITEM_ID_DIVIDER } = require('../config/menuItemIds'); //ID пунктов меню
const { shouldShowScanner, HARDWARE_SCANNER_TRIGGER_EVENT_NAME, HARDWARE_SCANNER_CAMERA_HIDE_MS } = require('../config/scannerConfig'); //Логика и константы сканера
const { DeviceEventEmitter } = require('react-native'); //События нативного модуля
const HardwareScannerBridge = require('../services/HardwareScannerBridge'); //Отключение фокуса у камеры при отображении
const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана
//-----------
@ -32,7 +48,7 @@ const styles = require('../styles/screens/MainScreen.styles'); //Стили эк
//Главный экран приложения
function MainScreen() {
const { showInfo, showError } = useAppMessagingContext();
const { showInfo, showError, state: messagingState } = useAppMessagingContext();
const { mode, setNotConnected } = useAppModeContext();
const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext();
const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
@ -41,7 +57,60 @@ function MainScreen() {
const [menuVisible, setMenuVisible] = React.useState(false);
const [serverUrl, setServerUrl] = React.useState('');
const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false);
const [mainScannerPriority, setMainScannerPriority] = React.useState('camera');
const [cameraResumedByTap, setCameraResumedByTap] = React.useState(false);
const [scanResult, setScanResult] = React.useState(null);
const [cameraHiddenForHardwareScan, setCameraHiddenForHardwareScan] = React.useState(false);
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
//Обработчик встроенного сканера: данные как есть в результат сканирования (все символы)
const handleHardwareScan = React.useCallback(barcode => {
if (barcode == null) return;
const value = typeof barcode === 'string' ? barcode : String(barcode);
setScanResult({ type: 'unknown', value: value });
setCameraHiddenForHardwareScan(false);
}, []);
const mainScanHandlerRef = React.useRef(handleHardwareScan);
mainScanHandlerRef.current = handleHardwareScan;
const stableMainScanHandler = React.useCallback(barcode => {
if (typeof mainScanHandlerRef.current === 'function') mainScanHandlerRef.current(barcode);
}, []);
//Регистрация обработчика встроенного сканера
React.useEffect(() => {
registerHandler(scannerScreens.MAIN, stableMainScanHandler);
return function cleanup() {
unregisterHandler(scannerScreens.MAIN);
};
}, [registerHandler, unregisterHandler, scannerScreens.MAIN, stableMainScanHandler]);
//При нажатии кнопки встроенного сканера (142/141): скрываем камеру (отложенно, вне контекста события)
React.useEffect(function subscribeHardwareScannerTrigger() {
let restoreTimeoutId = null;
const sub = DeviceEventEmitter.addListener(HARDWARE_SCANNER_TRIGGER_EVENT_NAME, function onTrigger() {
setTimeout(function applyTriggerState() {
setCameraResumedByTap(false);
setCameraHiddenForHardwareScan(true);
if (restoreTimeoutId != null) clearTimeout(restoreTimeoutId);
restoreTimeoutId = setTimeout(function restoreCamera() {
restoreTimeoutId = null;
setCameraHiddenForHardwareScan(false);
}, HARDWARE_SCANNER_CAMERA_HIDE_MS);
}, 0);
});
return function cleanup() {
sub.remove();
if (restoreTimeoutId != null) clearTimeout(restoreTimeoutId);
};
}, []);
//При отображении области сканера вызываем setCameraViewsNotFocusable
React.useEffect(() => {
if (!isScannerOpen) return;
HardwareScannerBridge.setCameraViewsNotFocusable();
}, [isScannerOpen]);
//Загрузка настроек главного экрана при готовности БД
React.useEffect(() => {
@ -58,9 +127,10 @@ function MainScreen() {
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
} catch (loadError) {
console.error('Ошибка загрузки настроек главного экрана:', loadError);
}
const savedPriority = await getSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY);
setMainScannerPriority(savedPriority === 'hardware' ? 'hardware' : 'camera');
} catch (loadError) {}
};
loadMainScreenSettings();
@ -99,38 +169,37 @@ function MainScreen() {
navigate(SCREENS.SETTINGS);
}, [navigate, SCREENS.SETTINGS]);
//Выполнение выхода (для диалога подтверждения)
const performLogout = React.useCallback(async () => {
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
if (result.success) {
setNotConnected();
setInitialScreen(SCREENS.AUTH);
} else {
showError(result.error || 'Ошибка выхода');
}
}, [logout, mode, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]);
//Обработчик выхода из приложения
const handleLogout = React.useCallback(() => {
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Выйти', performLogout);
showInfo('Вы уверены, что хотите выйти?', {
title: 'Подтверждение выхода',
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
});
}, [showInfo, performLogout]);
//Универсальный обработчик выхода
const { handleLogout } = React.useMemo(
() =>
createLogoutHandler({
logout,
mode,
setNotConnected,
setInitialScreen,
SCREENS,
showError,
showInfo,
logoutConfirmTitle: LOGOUT_CONFIRM_TITLE,
logoutConfirmMessage: LOGOUT_CONFIRM_MESSAGE,
logoutButtonTitle: AUTH_BUTTON_LOGOUT,
getConfirmButtonOptions,
dialogButtonTypeError: DIALOG_BUTTON_TYPE.ERROR,
dialogCancelButton: DIALOG_CANCEL_BUTTON
}),
[logout, mode, setNotConnected, setInitialScreen, SCREENS, showError, showInfo]
);
//Пункты бокового меню
const menuItems = React.useMemo(() => {
const items = [
{
id: 'settings',
id: MENU_ITEM_ID_SETTINGS,
title: MENU_ITEM_SETTINGS,
onPress: handleOpenSettings
},
{
id: 'about',
id: MENU_ITEM_ID_ABOUT,
title: MENU_ITEM_ABOUT,
onPress: handleShowAbout
}
@ -138,14 +207,14 @@ function MainScreen() {
//Добавляем разделитель перед кнопками авторизации
items.push({
id: 'divider',
id: MENU_ITEM_ID_DIVIDER,
type: 'divider'
});
//Кнопка "Вход" для оффлайн режима
if (mode === 'OFFLINE') {
items.push({
id: 'login',
id: MENU_ITEM_ID_LOGIN,
title: MENU_ITEM_LOGIN,
onPress: handleLogin
});
@ -154,10 +223,11 @@ function MainScreen() {
//Кнопка "Выход" для онлайн/оффлайн режима (когда есть сессия)
if ((mode === 'ONLINE' || mode === 'OFFLINE') && isAuthenticated) {
items.push({
id: 'logout',
id: MENU_ITEM_ID_LOGOUT,
title: MENU_ITEM_LOGOUT,
onPress: handleLogout,
textStyle: { color: APP_COLORS.error }
textStyle: { color: APP_COLORS.error },
iconColor: APP_COLORS.error
});
}
@ -175,18 +245,52 @@ function MainScreen() {
}, []);
//Видимость сканера
const isOfflineMessageVisible = Boolean(messagingState.visible && messagingState.title === OFFLINE_MODE_TITLE);
const isAboutMessageVisible = Boolean(messagingState.visible && messagingState.title === APP_ABOUT_TITLE);
const isScannerOpen = shouldShowScanner({
alwaysShowScanner,
hasScanResult: scanResult != null,
isStartupCheckInProgress: isStartupSessionCheckInProgress
isStartupCheckInProgress: isStartupSessionCheckInProgress,
isOfflineMessageVisible,
isAboutMessageVisible
});
const cameraShown = isScannerOpen && !cameraHiddenForHardwareScan;
const cameraActiveOnMain = !(alwaysShowScanner && mainScannerPriority === 'hardware') || cameraResumedByTap;
//Когда камера на главном экране видима
React.useEffect(
function cameraVisibleOnMain() {
if (!cameraShown) return;
const t1 = setTimeout(function run1() {
HardwareScannerBridge.setCameraViewsNotFocusable();
}, 100);
const t2 = setTimeout(function run2() {
HardwareScannerBridge.setCameraViewsNotFocusable();
}, 400);
return function cleanup() {
clearTimeout(t1);
clearTimeout(t2);
};
},
[cameraShown]
);
const handleResumeCameraByTap = React.useCallback(function resumeCameraByTap() {
setCameraResumedByTap(true);
}, []);
return (
<View style={styles.container}>
<AppHeader onMenuPress={handleMenuOpen} />
<View style={styles.content}>
<ScannerArea alwaysShowScanner={alwaysShowScanner} scannerOpen={isScannerOpen} onScanResult={handleScanResult} />
<ScannerArea
alwaysShowScanner={alwaysShowScanner}
scannerOpen={cameraShown}
cameraActive={cameraActiveOnMain}
onScanResult={handleScanResult}
onResumeCameraPress={mainScannerPriority === 'hardware' ? handleResumeCameraByTap : undefined}
/>
</View>
<SideMenu

View File

@ -19,18 +19,22 @@ const AppHeader = require('../components/layout/AppHeader'); //Заголово
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
const { AUTH_SETTINGS_KEYS, DEFAULT_SERVER_REQUEST_TIMEOUT } = require('../config/authConfig'); //Конфиг авторизации
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
const { getAppInfo, getModeLabel } = require('../utils/appInfo'); //Информация о приложении и режиме
const { validateServerUrlAllowEmpty, validateIdleTimeout } = require('../utils/validation'); //Валидация
const { validateServerUrlAllowEmpty, validateIdleTimeoutAllowEmpty, validateServerRequestTimeout } = require('../utils/validation'); //Валидация
const {
APP_ABOUT_TITLE,
SETTINGS_SERVER_SAVED_MESSAGE,
SETTINGS_RESET_SUCCESS_MESSAGE,
MENU_ITEM_ABOUT,
SCANNER_SETTING_LABEL
SCANNER_SETTING_LABEL,
SCANNER_PRIORITY_LABEL,
SCANNER_PRIORITY_CAMERA,
SCANNER_PRIORITY_HARDWARE
} = require('../config/messages'); //Сообщения
const styles = require('../styles/screens/SettingsScreen.styles'); //Стили экрана
@ -42,7 +46,7 @@ function SettingsScreen() {
const { showInfo, showError, showSuccess } = useAppMessagingContext();
const { APP_MODE, mode, setNotConnected } = useAppModeContext();
const { goBack, canGoBack } = useAppNavigationContext();
const { getSetting, setSetting, clearSettings, clearInspections, vacuum, isDbReady } = useAppLocalDbContext();
const { getSetting, setSetting, clearSettings, vacuum, isDbReady } = useAppLocalDbContext();
const { session, isAuthenticated, getDeviceId, setLastSavedServerUrlFromSettings } = useAppAuthContext();
const [serverUrl, setServerUrl] = React.useState('');
@ -52,11 +56,47 @@ function SettingsScreen() {
const [isLoading, setIsLoading] = React.useState(false);
const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
const [isIdleTimeoutDialogVisible, setIsIdleTimeoutDialogVisible] = React.useState(false);
const [serverRequestTimeout, setServerRequestTimeout] = React.useState('');
const [isServerRequestTimeoutDialogVisible, setIsServerRequestTimeoutDialogVisible] = React.useState(false);
const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false);
const [mainScannerPriority, setMainScannerPriority] = React.useState('camera');
const { registerHandler, unregisterHandler, SCREENS: scannerScreens } = useHardwareScannerContext();
const serverUrlDialogRef = React.useRef(null);
const idleTimeoutDialogRef = React.useRef(null);
const serverRequestTimeoutDialogRef = React.useRef(null);
//Предотвращение повторной загрузки настроек
const settingsLoadedRef = React.useRef(false);
//Обработчик встроенного сканера: только при открытом диалоге ввода — замена значения в поле (данные как есть); иначе ничего не делать
const handleHardwareScan = React.useCallback(
barcode => {
if (isServerUrlDialogVisible && serverUrlDialogRef.current) {
serverUrlDialogRef.current.setValueFromScanner(barcode);
} else if (isIdleTimeoutDialogVisible && idleTimeoutDialogRef.current) {
idleTimeoutDialogRef.current.setValueFromScanner(barcode);
} else if (isServerRequestTimeoutDialogVisible && serverRequestTimeoutDialogRef.current) {
serverRequestTimeoutDialogRef.current.setValueFromScanner(barcode);
}
},
[isServerUrlDialogVisible, isIdleTimeoutDialogVisible, isServerRequestTimeoutDialogVisible]
);
const settingsScanHandlerRef = React.useRef(handleHardwareScan);
settingsScanHandlerRef.current = handleHardwareScan;
const stableSettingsScanHandler = React.useCallback(barcode => {
if (typeof settingsScanHandlerRef.current === 'function') settingsScanHandlerRef.current(barcode);
}, []);
//Регистрация обработчика встроенного сканера на экране настроек
React.useEffect(() => {
registerHandler(scannerScreens.SETTINGS, stableSettingsScanHandler);
return function cleanup() {
unregisterHandler(scannerScreens.SETTINGS);
};
}, [registerHandler, unregisterHandler, scannerScreens.SETTINGS, stableSettingsScanHandler]);
//Загрузка сохраненных настроек при готовности БД
React.useEffect(() => {
//Выходим, если БД не готова или уже загрузили настройки
@ -78,13 +118,16 @@ function SettingsScreen() {
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
const savedIdleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
if (savedIdleTimeout) {
setIdleTimeout(savedIdleTimeout);
setIdleTimeout(savedIdleTimeout != null ? String(savedIdleTimeout).trim() : '');
const savedServerRequestTimeout = await getSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT);
const serverRequestTimeoutValue = savedServerRequestTimeout != null ? String(savedServerRequestTimeout).trim() : '';
if (serverRequestTimeoutValue) {
setServerRequestTimeout(serverRequestTimeoutValue);
} else {
//Устанавливаем значение по умолчанию
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
setIdleTimeout(defaultValue);
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
const defaultValue = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
setServerRequestTimeout(defaultValue);
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultValue);
}
//Получаем или генерируем идентификатор устройства
@ -93,8 +136,10 @@ function SettingsScreen() {
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
const savedPriority = await getSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY);
setMainScannerPriority(savedPriority === 'hardware' ? 'hardware' : 'camera');
} catch (error) {
console.error('Ошибка загрузки настроек:', error);
showError('Не удалось загрузить настройки');
} finally {
setIsLoading(false);
@ -125,6 +170,12 @@ function SettingsScreen() {
[]
);
//Стиль поля времени ожидания сервера при нажатии
const getServerRequestTimeoutFieldPressableStyle = React.useCallback(
({ pressed }) => [styles.serverUrlField, pressed && styles.serverUrlFieldPressed],
[]
);
//Открытие диалога ввода URL сервера (только в режиме "Не подключено")
const handleOpenServerUrlDialog = React.useCallback(() => {
if (!isServerUrlEditable) {
@ -159,7 +210,6 @@ function SettingsScreen() {
showError('Не удалось сохранить настройки');
}
} catch (error) {
console.error('Ошибка сохранения настроек:', error);
showError('Не удалось сохранить настройки');
}
@ -181,13 +231,41 @@ function SettingsScreen() {
showError('Не удалось сохранить настройку');
}
} catch (error) {
console.error('Ошибка сохранения настройки сканера:', error);
showError('Не удалось сохранить настройку');
}
},
[setSetting, showSuccess, showError]
);
//Выбор приоритета на главном экране: камера или встроенный сканер
const handleSelectScannerPriority = React.useCallback(
async value => {
if (value !== 'camera' && value !== 'hardware') return;
try {
const success = await setSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY, value);
if (success) {
setMainScannerPriority(value);
showSuccess('Настройка сохранена');
} else {
showError('Не удалось сохранить настройку');
}
} catch (error) {
showError('Не удалось сохранить настройку');
}
},
[setSetting, showSuccess, showError]
);
//Выбор приоритета: камера
const handleSelectCameraPriority = React.useCallback(() => {
handleSelectScannerPriority('camera');
}, [handleSelectScannerPriority]);
//Выбор приоритета: встроенный сканер
const handleSelectHardwarePriority = React.useCallback(() => {
handleSelectScannerPriority('hardware');
}, [handleSelectScannerPriority]);
//Переключатель скрытия URL сервера в окне логина
const handleToggleHideServerUrl = React.useCallback(
async value => {
@ -201,7 +279,6 @@ function SettingsScreen() {
showError('Не удалось сохранить настройку');
}
} catch (error) {
console.error('Ошибка сохранения настройки:', error);
showError('Не удалось сохранить настройку');
}
},
@ -218,6 +295,16 @@ function SettingsScreen() {
setIsIdleTimeoutDialogVisible(false);
}, []);
//Открытие диалога ввода времени ожидания ответа от сервера
const handleOpenServerRequestTimeoutDialog = React.useCallback(() => {
setIsServerRequestTimeoutDialogVisible(true);
}, []);
//Закрытие диалога ввода времени ожидания ответа от сервера
const handleCloseServerRequestTimeoutDialog = React.useCallback(() => {
setIsServerRequestTimeoutDialogVisible(false);
}, []);
//Сохранение времени простоя
const handleSaveIdleTimeout = React.useCallback(
async value => {
@ -235,7 +322,6 @@ function SettingsScreen() {
showError('Не удалось сохранить настройку');
}
} catch (error) {
console.error('Ошибка сохранения времени простоя:', error);
showError('Не удалось сохранить настройку');
}
@ -244,22 +330,37 @@ function SettingsScreen() {
[setSetting, showSuccess, showError]
);
//Выполнение очистки кэша (для диалога подтверждения)
const performClearCache = React.useCallback(async () => {
try {
const success = await clearInspections();
if (success) {
showSuccess('Кэш успешно очищен');
} else {
showError('Не удалось очистить кэш');
}
} catch (error) {
console.error('Ошибка очистки кэша:', error);
showError('Не удалось очистить кэш');
}
}, [showSuccess, showError, clearInspections]);
//Сохранение времени ожидания ответа от сервера
const handleSaveServerRequestTimeout = React.useCallback(
async value => {
setIsServerRequestTimeoutDialogVisible(false);
setIsLoading(true);
//Очистка кэша (осмотров)
try {
const trimmedValue = value != null ? String(value).trim() : '';
const success = await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, trimmedValue);
if (success) {
setServerRequestTimeout(trimmedValue);
showSuccess('Время ожидания сервера сохранено');
} else {
showError('Не удалось сохранить настройку');
}
} catch (error) {
showError('Не удалось сохранить настройку');
}
setIsLoading(false);
},
[setSetting, showSuccess, showError]
);
//Выполнение очистки кэша
const performClearCache = React.useCallback(async () => {
showSuccess('Кэш успешно очищен');
}, [showSuccess]);
//Очистка кэша — диалог подтверждения, по подтверждению ничего не выполняется
const handleClearCache = React.useCallback(() => {
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Очистить', performClearCache);
@ -273,29 +374,32 @@ function SettingsScreen() {
//Подключён (онлайн/офлайн): сбрасываем только непричастные к подключению настройки; не подключён: полный сброс
const performResetSettings = React.useCallback(async () => {
try {
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
if (mode === APP_MODE.NOT_CONNECTED) {
const success = await clearSettings();
if (success) {
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
setServerUrl('');
setHideServerUrl(false);
setAlwaysShowScanner(false);
setIdleTimeout(defaultValue);
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
setMainScannerPriority('camera');
setIdleTimeout('');
setServerRequestTimeout(defaultServerTimeout);
setNotConnected();
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
} else {
showError('Не удалось сбросить настройки');
}
} else {
//Подключён (онлайн или офлайн): сбрасываем только время простоя
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
setIdleTimeout(defaultValue);
//Подключён (онлайн или офлайн): сбрасываем только время простоя и таймаут сервера
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, '');
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
setIdleTimeout('');
setServerRequestTimeout(defaultServerTimeout);
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
}
} catch (error) {
console.error('Ошибка сброса настроек:', error);
showError('Не удалось сбросить настройки');
}
}, [
@ -304,6 +408,7 @@ function SettingsScreen() {
setServerUrl,
setHideServerUrl,
setIdleTimeout,
setServerRequestTimeout,
setNotConnected,
clearSettings,
setSetting,
@ -332,7 +437,6 @@ function SettingsScreen() {
showError('Не удалось оптимизировать базу данных');
}
} catch (error) {
console.error('Ошибка оптимизации БД:', error);
showError('Не удалось оптимизировать базу данных');
} finally {
setIsLoading(false);
@ -443,20 +547,46 @@ function SettingsScreen() {
</View>
</View>
<View style={styles.section}>
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
Главный экран
</AppText>
{mode === APP_MODE.ONLINE || mode === APP_MODE.OFFLINE ? (
<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 style={styles.switchRow}>
<AppSwitch
label={SCANNER_SETTING_LABEL}
value={alwaysShowScanner}
onValueChange={handleToggleAlwaysShowScanner}
disabled={isLoading || !isDbReady}
/>
</View>
{alwaysShowScanner ? (
<View style={styles.prioritySection}>
<AppText style={styles.fieldLabel} variant="caption" weight="medium">
{SCANNER_PRIORITY_LABEL}
</AppText>
<Pressable
style={[styles.priorityRow, mainScannerPriority === 'camera' && styles.priorityRowSelected]}
onPress={handleSelectCameraPriority}
disabled={isLoading || !isDbReady}
>
<AppText style={styles.priorityRowText}>{SCANNER_PRIORITY_CAMERA}</AppText>
{mainScannerPriority === 'camera' ? <AppText style={styles.priorityCheck}></AppText> : null}
</Pressable>
<Pressable
style={[styles.priorityRow, mainScannerPriority === 'hardware' && styles.priorityRowSelected]}
onPress={handleSelectHardwarePriority}
disabled={isLoading || !isDbReady}
>
<AppText style={styles.priorityRowText}>{SCANNER_PRIORITY_HARDWARE}</AppText>
{mainScannerPriority === 'hardware' ? <AppText style={styles.priorityCheck}></AppText> : null}
</Pressable>
</View>
) : null}
</View>
</View>
) : null}
<View style={styles.section}>
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
@ -468,8 +598,22 @@ function SettingsScreen() {
</AppText>
<Pressable style={getIdleTimeoutFieldPressableStyle} onPress={handleOpenIdleTimeoutDialog} disabled={isLoading || !isDbReady}>
<AppText style={[styles.serverUrlText, !idleTimeout && styles.serverUrlPlaceholder]} numberOfLines={1}>
{idleTimeout || 'Не задано'}
</AppText>
</Pressable>
<AppText style={[styles.fieldLabel, styles.fieldLabelMarginTop]} variant="caption" weight="medium">
Максимальное время ожидания ответа от сервера (секунд)
</AppText>
<Pressable
style={getServerRequestTimeoutFieldPressableStyle}
onPress={handleOpenServerRequestTimeoutDialog}
disabled={isLoading || !isDbReady}
>
<AppText style={styles.serverUrlText} numberOfLines={1}>
{idleTimeout || String(DEFAULT_IDLE_TIMEOUT)}
{serverRequestTimeout || String(DEFAULT_SERVER_REQUEST_TIMEOUT)}
</AppText>
</Pressable>
@ -562,6 +706,7 @@ function SettingsScreen() {
</ScrollView>
<InputDialog
ref={serverUrlDialogRef}
visible={isServerUrlDialogVisible}
title="Адрес сервера"
label="URL сервера приложений"
@ -576,17 +721,33 @@ function SettingsScreen() {
/>
<InputDialog
ref={idleTimeoutDialogRef}
visible={isIdleTimeoutDialogVisible}
title="Время простоя"
label="Максимальное время простоя (минут)"
value={idleTimeout}
placeholder="Например: 30"
placeholder="Например: 30 или пусто"
keyboardType="numeric"
confirmText="Сохранить"
cancelText="Отмена"
onConfirm={handleSaveIdleTimeout}
onCancel={handleCloseIdleTimeoutDialog}
validator={validateIdleTimeout}
validator={validateIdleTimeoutAllowEmpty}
/>
<InputDialog
ref={serverRequestTimeoutDialogRef}
visible={isServerRequestTimeoutDialogVisible}
title="Время ожидания сервера"
label="Максимальное время ожидания ответа от сервера (секунд)"
value={serverRequestTimeout}
placeholder="Например: 60"
keyboardType="numeric"
confirmText="Сохранить"
cancelText="Отмена"
onConfirm={handleSaveServerRequestTimeout}
onCancel={handleCloseServerRequestTimeoutDialog}
validator={validateServerRequestTimeout}
/>
</AdaptiveView>
);

View File

@ -0,0 +1,78 @@
/*
Предрейсовые осмотры - мобильное приложение
Мост встроенного сканера
*/
//---------------------
//Подключение библиотек
//---------------------
const { NativeModules, Platform, DeviceEventEmitter } = require('react-native');
const { HARDWARE_SCANNER_EVENT_NAME } = require('../config/scannerConfig');
//---------
//Состояние
//---------
let hardwareCallback = null;
let isListening = false;
const { HardwareScannerModule } = NativeModules;
//-----------
//Тело модуля
//-----------
//Доставка данных в колбэк в следующем тике (данные как есть, без обрезки)
function deliverBarcode(barcode) {
if (!isListening || !hardwareCallback) return;
const s = typeof barcode === 'string' ? barcode : '';
if (s.length === 0) return;
setTimeout(function deliverNextTick() {
if (!isListening || !hardwareCallback) return;
try {
hardwareCallback(s);
} catch (_err) {}
}, 0);
}
//Запуск прослушивания встроенного сканера (событие по мосту); возвращает функцию отписки
function startHardwareListener(onData) {
if (Platform.OS !== 'android') return function noopUnsubscribe() {};
if (!HardwareScannerModule) return function noopUnsubscribe() {};
if (typeof onData !== 'function') return function noopUnsubscribe() {};
if (isListening && hardwareCallback) return function noopUnsubscribe() {};
hardwareCallback = onData;
isListening = true;
const subscription = DeviceEventEmitter.addListener(HARDWARE_SCANNER_EVENT_NAME, function onBarcodeEvent(event) {
const barcode = event && event.barcode != null ? String(event.barcode) : '';
deliverBarcode(barcode);
});
return function unsubscribe() {
isListening = false;
if (subscription && typeof subscription.remove === 'function') subscription.remove();
hardwareCallback = null;
};
}
//Отключить фокус у видов камеры в иерархии, чтобы встроенный сканер получал клавиши при отображении камеры (только Android)
function setCameraViewsNotFocusable() {
if (Platform.OS !== 'android' || !HardwareScannerModule) return;
try {
if (typeof HardwareScannerModule.setCameraViewsNotFocusable === 'function') {
HardwareScannerModule.setCameraViewsNotFocusable();
}
} catch (_) {}
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
startHardwareListener,
setCameraViewsNotFocusable
};

View File

@ -83,14 +83,17 @@ const styles = StyleSheet.create({
color: APP_COLORS.textSecondary
},
input: {
height: UI.INPUT_HEIGHT,
minHeight: UI.INPUT_HEIGHT,
borderWidth: 1,
borderColor: APP_COLORS.borderMedium,
borderRadius: UI.BORDER_RADIUS,
paddingHorizontal: responsiveSpacing(3),
paddingVertical: responsiveSpacing(2.5),
fontSize: UI.FONT_SIZE_MD,
color: APP_COLORS.textPrimary,
backgroundColor: APP_COLORS.surface
backgroundColor: APP_COLORS.surface,
includeFontPadding: false,
textAlignVertical: 'center'
},
inputFocused: {
borderColor: APP_COLORS.primary,

View File

@ -0,0 +1,26 @@
/*
Предрейсовые осмотры - мобильное приложение
Стили обёртки провайдера простоя
*/
//---------------------
//Подключение библиотек
//---------------------
const { StyleSheet } = require('react-native'); //StyleSheet
//-----------
//Тело модуля
//-----------
const styles = StyleSheet.create({
touchArea: {
flex: 1
}
});
//----------------
//Интерфейс модуля
//----------------
module.exports = styles;

View File

@ -0,0 +1,112 @@
/*
Предрейсовые осмотры - мобильное приложение
Стили компонента иконки пункта меню
*/
//---------------------
//Подключение библиотек
//---------------------
const { StyleSheet } = require('react-native'); //StyleSheet React Native
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
//-----------
//Тело модуля
//-----------
//Базовый размер контейнера иконки (должен совпадать с menuItemIcon в MenuItem.styles)
const ICON_SIZE = responsiveSize(24);
//Стили компонента иконки пункта меню
const styles = StyleSheet.create({
container: {
width: ICON_SIZE,
height: ICON_SIZE,
alignItems: 'center',
justifyContent: 'center'
},
//Иконка «Настройки» (шестерёнка): центральный круг
settingsCircle: {
position: 'absolute',
width: responsiveSize(10),
height: responsiveSize(10),
borderRadius: responsiveSize(5),
borderWidth: responsiveSize(2),
borderColor: APP_COLORS.textPrimary
},
settingsTooth: {
position: 'absolute',
width: responsiveSize(2),
height: responsiveSize(6),
backgroundColor: APP_COLORS.textPrimary,
borderRadius: 1
},
//Иконка «О приложении» (информация): круг с буквой i
aboutCircle: {
width: responsiveSize(18),
height: responsiveSize(18),
borderRadius: responsiveSize(9),
borderWidth: responsiveSize(2),
borderColor: APP_COLORS.textPrimary,
alignItems: 'center',
justifyContent: 'center'
},
aboutText: {
fontSize: responsiveSize(12),
fontWeight: '700',
color: APP_COLORS.textPrimary
},
//Иконка «Вход»: рамка и стрелка вправо
loginFrame: {
width: responsiveSize(14),
height: responsiveSize(14),
borderWidth: responsiveSize(2),
borderColor: APP_COLORS.textPrimary,
borderRadius: responsiveSize(2),
alignItems: 'center',
justifyContent: 'center'
},
loginArrow: {
width: 0,
height: 0,
borderTopWidth: responsiveSize(4),
borderBottomWidth: responsiveSize(4),
borderLeftWidth: responsiveSize(5),
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderLeftColor: APP_COLORS.textPrimary,
marginLeft: responsiveSize(2)
},
//Иконка «Выход»: рамка и стрелка влево
logoutFrame: {
width: responsiveSize(14),
height: responsiveSize(14),
borderWidth: responsiveSize(2),
borderColor: APP_COLORS.textPrimary,
borderRadius: responsiveSize(2),
alignItems: 'center',
justifyContent: 'center'
},
logoutArrow: {
width: 0,
height: 0,
borderTopWidth: responsiveSize(4),
borderBottomWidth: responsiveSize(4),
borderRightWidth: responsiveSize(5),
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
borderRightColor: APP_COLORS.textPrimary,
marginRight: responsiveSize(2)
}
});
//----------------
//Интерфейс модуля
//----------------
module.exports = styles;

View File

@ -0,0 +1,48 @@
/*
Предрейсовые осмотры - мобильное приложение
Стили оверлея «камера на паузе» (серая область с подсказкой)
*/
//---------------------
//Подключение библиотек
//---------------------
const { StyleSheet } = require('react-native'); //StyleSheet React Native
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема
const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
const { UI } = require('../../config/appConfig'); //Конфигурация UI
//---------
//Константы
//---------
//Фон оверлея: серый с прозрачностью (область камеры на паузе)
const OVERLAY_BACKGROUND = 'rgba(96, 96, 96, 0.65)';
//-----------
//Тело модуля
//-----------
//Стили оверлея паузы камеры
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: OVERLAY_BACKGROUND,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: responsiveSpacing(4),
borderRadius: UI.BORDER_RADIUS
},
message: {
color: APP_COLORS.textInverse,
fontSize: UI.FONT_SIZE_MD,
textAlign: 'center',
maxWidth: '100%'
}
});
//----------------
//Интерфейс модуля
//----------------
module.exports = styles;

View File

@ -7,7 +7,7 @@
//Подключение библиотек
//---------------------
const { StyleSheet, Platform } = require('react-native'); //StyleSheet React Native
const { StyleSheet, Platform, Dimensions } = require('react-native'); //StyleSheet React Native
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
const { UI } = require('../../config/appConfig'); //Конфигурация UI
const { responsiveSpacing, widthPercentage } = require('../../utils/responsive'); //Адаптивные утилиты
@ -16,18 +16,50 @@ const { responsiveSpacing, widthPercentage } = require('../../utils/responsive')
//Тело модуля
//-----------
const windowSize = Dimensions.get('window');
const resultCardWidth = widthPercentage(92);
//Стили модального окна результата сканирования
const styles = StyleSheet.create({
backdrop: {
modalRoot: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: APP_COLORS.overlay
},
overlayContent: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1001
},
overlay: {
position: 'absolute',
left: 0,
top: 0,
width: windowSize.width,
height: windowSize.height,
backgroundColor: APP_COLORS.overlay,
zIndex: 1000
},
backdrop: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: UI.PADDING
},
container: {
width: '100%',
maxWidth: widthPercentage(90),
width: resultCardWidth,
minWidth: resultCardWidth,
maxWidth: resultCardWidth,
backgroundColor: APP_COLORS.surface,
borderRadius: UI.BORDER_RADIUS,
...Platform.select({
@ -83,6 +115,7 @@ const styles = StyleSheet.create({
color: APP_COLORS.textSecondary
},
valueBlock: {
width: '100%',
paddingVertical: responsiveSpacing(2),
paddingHorizontal: responsiveSpacing(3),
backgroundColor: APP_COLORS.surfaceAlt,
@ -90,6 +123,7 @@ const styles = StyleSheet.create({
marginBottom: responsiveSpacing(4)
},
valueText: {
width: '100%',
fontSize: UI.FONT_SIZE_MD,
color: APP_COLORS.textPrimary
},

View File

@ -23,6 +23,14 @@ const styles = StyleSheet.create({
minHeight: responsiveSpacing(30),
marginBottom: responsiveSpacing(2)
},
scannerWrapper: {
flex: 1,
width: '100%'
},
//Область тапа для включения камеры (оверлей поверх сканера на паузе)
cameraPausedOverlayTouchable: {
...StyleSheet.absoluteFillObject
},
modalContainer: {
flex: 1
},

View File

@ -28,6 +28,15 @@ const styles = StyleSheet.create({
},
button: {
minWidth: responsiveSpacing(40)
},
hint: {
textAlign: 'center',
color: APP_COLORS.textSecondary,
marginBottom: responsiveSpacing(3)
},
secondaryButton: {
marginTop: responsiveSpacing(2),
minWidth: responsiveSpacing(40)
}
});

View File

@ -56,6 +56,16 @@ const styles = StyleSheet.create({
color: APP_COLORS.white,
fontWeight: '600'
},
logoutButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: APP_COLORS.error,
marginTop: responsiveSpacing(3)
},
logoutButtonText: {
color: APP_COLORS.error,
fontWeight: '600'
},
hint: {
textAlign: 'center',
marginTop: responsiveSpacing(4),

View File

@ -88,6 +88,33 @@ const styles = StyleSheet.create({
switchRow: {
marginTop: responsiveSpacing(3)
},
prioritySection: {
marginTop: responsiveSpacing(4)
},
priorityRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: responsiveSpacing(2.5),
paddingHorizontal: responsiveSpacing(2),
marginTop: responsiveSpacing(2),
borderRadius: UI.BORDER_RADIUS,
borderWidth: 1,
borderColor: APP_COLORS.borderSubtle
},
priorityRowSelected: {
borderColor: APP_COLORS.primary,
backgroundColor: APP_COLORS.primaryExtraLight
},
priorityRowText: {
flex: 1,
fontSize: UI.FONT_SIZE_MD,
color: APP_COLORS.textPrimary
},
priorityCheck: {
fontSize: UI.FONT_SIZE_MD,
color: APP_COLORS.primary
},
actionButton: {
marginTop: responsiveSpacing(3)
},

View File

@ -0,0 +1,53 @@
/*
Предрейсовые осмотры - мобильное приложение
Порядок и доступность полей формы аутентификации для подстановки данных сканера
*/
//---------------------
//Подключение библиотек
//---------------------
const { AUTH_FORM_FIELD_ORDER } = require('../config/authFormFieldsConfig'); //Порядок полей формы
const { resolveScanTarget, getNextInOrder } = require('./scanInputTargetResolver'); //Резолвер цели для сканера
//-----------
//Тело модуля
//-----------
//Проверка доступности поля по опциям (видимость и редактируемость)
function isFieldAllowed(fieldId, options) {
if (!options || typeof options !== 'object') return false;
const { serverVisible, serverEditable, loginEditable, passwordEditable } = options;
if (fieldId === 'server') return Boolean(serverVisible && serverEditable);
if (fieldId === 'login') return Boolean(loginEditable);
if (fieldId === 'password') return Boolean(passwordEditable);
return false;
}
//Возвращает список идентификаторов полей формы, доступных для ввода (видимых и не заблокированных)
function getAllowedAuthFields(options) {
if (!options || typeof options !== 'object') {
return [];
}
return AUTH_FORM_FIELD_ORDER.filter(fieldId => isFieldAllowed(fieldId, options));
}
//Возвращает идентификатор следующего поля после текущего в списке доступных, либо null (после последнего — отправка формы)
function getNextAuthField(currentFieldId, allowedFields) {
return getNextInOrder(currentFieldId, allowedFields);
}
//Определяет поле для первого сегмента сканера
function getScannerInitialTarget(focusedFieldId, allowedFields) {
return resolveScanTarget(focusedFieldId, allowedFields);
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
getAllowedAuthFields,
getNextAuthField,
getScannerInitialTarget
};

View File

@ -0,0 +1,31 @@
/*
Предрейсовые осмотры - мобильное приложение
Утилиты разбора данных от встроенного сканера на экране аутентификации
*/
//-----------
//Тело модуля
//-----------
//Разделители строк (Enter) в данных штрихкода (CRLF и LF)
const LINE_BREAK_REGEX = /\r?\n/;
//Разбивает отсканированную строку на сегменты по Enter (данные как есть, без trim)
function parseScannedSegmentsForAuth(rawString) {
if (rawString == null || typeof rawString !== 'string') {
return [];
}
if (rawString === '') {
return [];
}
return rawString.split(LINE_BREAK_REGEX);
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
parseScannedSegmentsForAuth,
LINE_BREAK_REGEX
};

View File

@ -107,8 +107,7 @@ const getAndroidId = async () => {
}
return null;
} catch (error) {
console.warn('Не удалось получить Android ID:', error);
} catch (_error) {
return null;
}
};
@ -134,8 +133,7 @@ const getIosId = async () => {
}
return null;
} catch (error) {
console.warn('Не удалось получить iOS ID:', error);
} catch (_error) {
return null;
}
};

View File

@ -0,0 +1,61 @@
/*
Предрейсовые осмотры - мобильное приложение
Универсальный поток выхода из приложения
*/
//---------------------
//Подключение библиотек
//---------------------
//-----------
//Тело модуля
//-----------
//Обработчики: выполнение выхода и показ диалога подтверждения
function createLogoutHandler(deps) {
const {
logout,
mode,
setNotConnected,
setInitialScreen,
SCREENS,
showError,
showInfo,
logoutConfirmTitle,
logoutConfirmMessage,
logoutButtonTitle,
getConfirmButtonOptions,
dialogButtonTypeError,
dialogCancelButton
} = deps;
const performLogout = async () => {
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
if (result.success) {
setNotConnected();
setInitialScreen(SCREENS.AUTH);
} else {
showError(result.error || 'Ошибка выхода');
}
};
const handleLogout = () => {
const confirmButton = getConfirmButtonOptions(dialogButtonTypeError, logoutButtonTitle, performLogout);
showInfo(logoutConfirmMessage, {
title: logoutConfirmTitle,
buttons: [dialogCancelButton, confirmButton]
});
};
return { performLogout, handleLogout };
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
createLogoutHandler
};

23
rn/app/src/utils/noop.js Normal file
View File

@ -0,0 +1,23 @@
/*
Предрейсовые осмотры - мобильное приложение
Пустые функции для использования в обработчиках и .catch()
*/
//---------
//Функции
//---------
//Пустая функция (например для onPress, чтобы не передавать клик дальше)
function noop() {}
//Пустой обработчик для .catch() — подавляет ошибку без побочных эффектов
function noopCatch() {}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
noop,
noopCatch
};

View File

@ -19,7 +19,7 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
//Проверка на старые устройства Android
const isLegacyAndroid = () => {
return Platform.OS === 'android' && Platform.Version < 26; // Android 8.0 = API 26
return Platform.OS === 'android' && Platform.Version < 26; //Android 8.0 = API 26
};
//Нормализация размера для разных плотностей пикселей
@ -99,7 +99,7 @@ const isLargeScreen = () => {
//Адаптивный отступ
const responsiveSpacing = (size = 1) => {
const baseSpacing = 4; // 4px базовый отступ
const baseSpacing = 4; //4px базовый отступ
return responsiveSize(baseSpacing * size);
};

View File

@ -0,0 +1,39 @@
/*
Предрейсовые осмотры - мобильное приложение
Разрешение цели для данных встроенного сканера
*/
//-----------
//Тело модуля
//-----------
//Определяет поле ввода для подстановки: сфокусированное (если в списке доступных), иначе первое доступное
function resolveScanTarget(focusedFieldId, allowedFieldIds) {
if (!allowedFieldIds || !Array.isArray(allowedFieldIds) || allowedFieldIds.length === 0) {
return null;
}
if (focusedFieldId != null && allowedFieldIds.indexOf(focusedFieldId) >= 0) {
return focusedFieldId;
}
return allowedFieldIds[0];
}
//Возвращает идентификатор следующего поля в порядке списка после текущего, либо null
function getNextInOrder(currentFieldId, allowedFieldIds) {
if (!allowedFieldIds || !Array.isArray(allowedFieldIds) || allowedFieldIds.length === 0) {
return null;
}
const idx = allowedFieldIds.indexOf(currentFieldId);
if (idx < 0) return null;
const nextIdx = idx + 1;
return nextIdx < allowedFieldIds.length ? allowedFieldIds[nextIdx] : null;
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
resolveScanTarget,
getNextInOrder
};

View File

@ -112,7 +112,6 @@ const encryptData = (data, secretKey, salt = '') => {
}
if (!secretKey) {
console.error('Ошибка шифрования: секретный ключ не предоставлен');
return '';
}
@ -121,7 +120,6 @@ const encryptData = (data, secretKey, salt = '') => {
const encrypted = xorEncrypt(data, keyHash);
return ENCRYPTED_PREFIX + encrypted;
} catch (error) {
console.error('Ошибка шифрования:', error);
return '';
}
};
@ -133,7 +131,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
}
if (!secretKey) {
console.error('Ошибка расшифровки: секретный ключ не предоставлен');
return '';
}
@ -148,7 +145,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
const keyHash = generateKeyHash(secretKey, salt);
return xorDecrypt(encryptedHex, keyHash);
} catch (error) {
console.error('Ошибка расшифровки:', error);
return '';
}
};

View File

@ -0,0 +1,40 @@
/*
Предрейсовые осмотры - мобильное приложение
Утилита таймаута запросов к серверу
*/
//---------------------
//Подключение библиотек
//---------------------
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
//-----------
//Тело модуля
//-----------
//Возвращает максимальное время ожидания ответа от сервера в миллисекундах (Promise)
async function getServerRequestTimeoutMs(getSetting) {
try {
const raw = await getSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT);
const trimmed = raw != null ? String(raw).trim() : '';
if (trimmed === '') {
return 0;
}
const sec = parseInt(trimmed, 10);
if (isNaN(sec) || sec <= 0) {
return 0;
}
return sec * 1000;
} catch (err) {
return 0;
}
}
//----------------
//Интерфейс модуля
//----------------
module.exports = {
getServerRequestTimeoutMs
};

View File

@ -19,7 +19,10 @@ const VALIDATION_MESSAGES = {
SERVER_URL_INVALID_SHORT: 'Некорректный формат URL',
IDLE_TIMEOUT_EMPTY: 'Введите время простоя',
IDLE_TIMEOUT_MIN: 'Введите положительное число (минимум 1 минута)',
IDLE_TIMEOUT_MAX: 'Максимальное значение: 1440 минут (24 часа)'
IDLE_TIMEOUT_MAX: 'Максимальное значение: 1440 минут (24 часа)',
SERVER_REQUEST_TIMEOUT_EMPTY: 'Введите время ожидания ответа от сервера',
SERVER_REQUEST_TIMEOUT_MIN: 'Значение должно быть больше 1 (минимум 1 секунда)',
SERVER_REQUEST_TIMEOUT_MAX: 'Максимальное значение: 600 секунд (10 минут)'
};
//-----------
@ -87,6 +90,42 @@ function validateIdleTimeout(value, options) {
return true;
}
//Валидация времени простоя с допуском пустого или нуля
function validateIdleTimeoutAllowEmpty(value, options) {
const trimmed = value != null ? String(value).trim() : '';
if (trimmed === '') {
return true;
}
const num = parseInt(trimmed, 10);
if (num === 0) {
return true;
}
return validateIdleTimeout(value, options);
}
//Валидация времени ожидания ответа от сервера (секунды)
function validateServerRequestTimeout(value, options) {
const opts = options || {};
const min = opts.min !== undefined ? opts.min : 1;
const max = opts.max !== undefined ? opts.max : 600;
const emptyMessage = opts.emptyMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_EMPTY;
const minMessage = opts.minMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_MIN;
const maxMessage = opts.maxMessage || VALIDATION_MESSAGES.SERVER_REQUEST_TIMEOUT_MAX;
const trimmed = value != null ? String(value).trim() : '';
if (trimmed === '') {
return emptyMessage;
}
const num = parseInt(trimmed, 10);
if (isNaN(num) || num < min) {
return minMessage;
}
if (num > max) {
return maxMessage;
}
return true;
}
//----------------
//Интерфейс модуля
//----------------
@ -96,5 +135,7 @@ module.exports = {
normalizeServerUrl,
validateServerUrl,
validateServerUrlAllowEmpty,
validateIdleTimeout
validateIdleTimeout,
validateIdleTimeoutAllowEmpty,
validateServerRequestTimeout
};