Рефакторинг. Добавление иконок для пунктов меню. Изменение папки файлов приложения (с app на parus_pre_trip_inspections). Доработка работы встроенного сканера устройства.
This commit is contained in:
parent
7bc9ddb898
commit
5449f61c6e
@ -13,8 +13,9 @@ const AppMessagingProvider = require('./src/components/layout/AppMessagingProvid
|
|||||||
const AppNavigationProvider = require('./src/components/layout/AppNavigationProvider').AppNavigationProvider; //Провайдер навигации
|
const AppNavigationProvider = require('./src/components/layout/AppNavigationProvider').AppNavigationProvider; //Провайдер навигации
|
||||||
const AppModeProvider = require('./src/components/layout/AppModeProvider').AppModeProvider; //Провайдер режима работы
|
const AppModeProvider = require('./src/components/layout/AppModeProvider').AppModeProvider; //Провайдер режима работы
|
||||||
const AppLocalDbProvider = require('./src/components/layout/AppLocalDbProvider').AppLocalDbProvider; //Провайдер локальной БД
|
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 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 приложения
|
const AppRoot = require('./src/components/layout/AppRoot'); //Корневой layout приложения
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
@ -28,13 +29,15 @@ function App() {
|
|||||||
<AppMessagingProvider>
|
<AppMessagingProvider>
|
||||||
<AppNavigationProvider>
|
<AppNavigationProvider>
|
||||||
<AppLocalDbProvider>
|
<AppLocalDbProvider>
|
||||||
|
<AppAuthProvider>
|
||||||
<AppModeProvider>
|
<AppModeProvider>
|
||||||
<AppServerProvider>
|
<AppServerProvider>
|
||||||
<AppPreTripInspectionsProvider>
|
<AppIdleProvider>
|
||||||
<AppRoot />
|
<AppRoot />
|
||||||
</AppPreTripInspectionsProvider>
|
</AppIdleProvider>
|
||||||
</AppServerProvider>
|
</AppServerProvider>
|
||||||
</AppModeProvider>
|
</AppModeProvider>
|
||||||
|
</AppAuthProvider>
|
||||||
</AppLocalDbProvider>
|
</AppLocalDbProvider>
|
||||||
</AppNavigationProvider>
|
</AppNavigationProvider>
|
||||||
</AppMessagingProvider>
|
</AppMessagingProvider>
|
||||||
|
|||||||
@ -77,9 +77,9 @@ android {
|
|||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
namespace "com.app"
|
namespace "com.parus_pre_trip_inspections"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.app"
|
applicationId "com.parus_pre_trip_inspections"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
@ -117,3 +117,19 @@ dependencies {
|
|||||||
implementation jscFlavor
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
android:usesCleartextTraffic="${usesCleartextTraffic}"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.app
|
package com.parus_pre_trip_inspections
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
@ -14,8 +14,7 @@ class MainApplication : Application(), ReactApplication {
|
|||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
packageList =
|
packageList =
|
||||||
PackageList(this).packages.apply {
|
PackageList(this).packages.apply {
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
add(HardwareScannerPackage())
|
||||||
// add(MyReactNativePackage())
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -11,10 +11,6 @@ const { AppRegistry } = require("react-native"); //Регистрация кор
|
|||||||
const App = require("./App"); //Корневой компонент приложения
|
const App = require("./App"); //Корневой компонент приложения
|
||||||
const { name: appName } = require("./app.json"); //Имя приложения
|
const { name: appName } = require("./app.json"); //Имя приложения
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
//Регистрация корневого компонента
|
//Регистрация корневого компонента
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
|
||||||
|
|||||||
7
rn/app/react-native.config.js
Normal file
7
rn/app/react-native.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
project: {
|
||||||
|
android: {
|
||||||
|
packageName: 'com.parus_pre_trip_inspections',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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(() => {
|
const handlePress = React.useCallback(() => {
|
||||||
if (!disabled && typeof onPress === 'function') onPress();
|
if (!disabled && typeof onPress === 'function') onPress();
|
||||||
}, [disabled, 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]);
|
const getPressableStyle = React.useMemo(() => getAppButtonPressableStyle(styles, disabled, style), [disabled, style]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle}>
|
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle} focusable={focusable}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<AppText style={[styles.text, textStyle]}>{title}</AppText>
|
<AppText style={[styles.text, textStyle]}>{title}</AppText>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -32,21 +32,31 @@ const AppInput = React.forwardRef(function AppInput(
|
|||||||
style,
|
style,
|
||||||
inputStyle,
|
inputStyle,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
|
onFocus: onFocusProp,
|
||||||
|
onBlur: onBlurProp,
|
||||||
...restProps
|
...restProps
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const [isFocused, setIsFocused] = React.useState(false);
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
|
|
||||||
//Обработчик фокуса
|
//Обработчик фокуса (внутренний + опциональный внешний)
|
||||||
const handleFocus = React.useCallback(() => {
|
const handleFocus = React.useCallback(
|
||||||
|
e => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
}, []);
|
if (typeof onFocusProp === 'function') onFocusProp(e);
|
||||||
|
},
|
||||||
|
[onFocusProp]
|
||||||
|
);
|
||||||
|
|
||||||
//Обработчик потери фокуса
|
//Обработчик потери фокуса (внутренний + опциональный внешний)
|
||||||
const handleBlur = React.useCallback(() => {
|
const handleBlur = React.useCallback(
|
||||||
|
e => {
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
}, []);
|
if (typeof onBlurProp === 'function') onBlurProp(e);
|
||||||
|
},
|
||||||
|
[onBlurProp]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, style]}>
|
||||||
|
|||||||
@ -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 }) {
|
function AppMessageButton({ title, onPress, onDismiss, buttonStyle, textStyle }) {
|
||||||
//Обработчик нажатия - вызывает onPress и закрывает диалог
|
//Обработчик нажатия - вызывает onPress и закрывает диалог
|
||||||
@ -95,20 +117,7 @@ function AppMessage({
|
|||||||
<View style={[styles.content, contentStyle]}>
|
<View style={[styles.content, contentStyle]}>
|
||||||
<Text style={[styles.message, messageStyle]}>{message}</Text>
|
<Text style={[styles.message, messageStyle]}>{message}</Text>
|
||||||
</View>
|
</View>
|
||||||
{hasButtons ? (
|
{hasButtons ? <View style={styles.buttonsRow}>{getMessageButtonElements(buttons, handleClose)}</View> : null}
|
||||||
<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}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@ -48,8 +48,6 @@ function CopyButton({ value, onCopy, onError, disabled = false, style }) {
|
|||||||
onCopy(value);
|
onCopy(value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка копирования в буфер:', error);
|
|
||||||
|
|
||||||
if (typeof onError === 'function') {
|
if (typeof onError === 'function') {
|
||||||
onError(error);
|
onError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,17 +8,20 @@
|
|||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
const React = require('react'); //React
|
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 AppText = require('./AppText'); //Общий текстовый компонент
|
||||||
const AppButton = require('./AppButton'); //Кнопка
|
const AppButton = require('./AppButton'); //Кнопка
|
||||||
const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
|
const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
|
||||||
|
|
||||||
|
const OVERLAY_Z = 9999;
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
//Тело модуля
|
//Тело модуля
|
||||||
//-----------
|
//-----------
|
||||||
|
|
||||||
//Модальное окно с полем ввода
|
//Модальное окно с полем ввода
|
||||||
function InputDialog({
|
function InputDialog(
|
||||||
|
{
|
||||||
visible,
|
visible,
|
||||||
title = 'Ввод данных',
|
title = 'Ввод данных',
|
||||||
label,
|
label,
|
||||||
@ -32,20 +35,73 @@ function InputDialog({
|
|||||||
onCancel,
|
onCancel,
|
||||||
validator,
|
validator,
|
||||||
errorMessage
|
errorMessage
|
||||||
}) {
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
//Локальное значение для редактирования
|
//Локальное значение для редактирования
|
||||||
const [inputValue, setInputValue] = React.useState(value);
|
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 [error, setError] = React.useState('');
|
||||||
const [isFocused, setIsFocused] = React.useState(false);
|
const [isFocused, setIsFocused] = React.useState(false);
|
||||||
|
|
||||||
//Сброс значения при открытии диалога
|
//Сброс значения и фокусируемости только при открытии диалога
|
||||||
|
const prevVisibleRef = React.useRef(false);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (visible) {
|
const justOpened = visible && !prevVisibleRef.current;
|
||||||
|
prevVisibleRef.current = visible;
|
||||||
|
if (justOpened) {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
setError('');
|
setError('');
|
||||||
|
if (Platform.OS === 'android') setInputFocusable(false);
|
||||||
}
|
}
|
||||||
}, [visible, value]);
|
}, [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(() => {
|
const handleFocus = React.useCallback(() => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
@ -97,13 +153,22 @@ function InputDialog({
|
|||||||
handleCancel();
|
handleCancel();
|
||||||
}, [handleCancel]);
|
}, [handleCancel]);
|
||||||
|
|
||||||
return (
|
const preventKeyActions = Platform.OS === 'android';
|
||||||
<Modal visible={visible} transparent={true} animationType="fade" statusBarTranslucent={true} onRequestClose={handleRequestClose}>
|
const closePressableProps = preventKeyActions ? { focusable: false } : {};
|
||||||
<View style={styles.backdrop}>
|
const cancelButtonFocusable = preventKeyActions ? false : undefined;
|
||||||
|
const confirmButtonFocusable = preventKeyActions ? false : undefined;
|
||||||
|
|
||||||
|
const dialogContent = (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<AppText style={styles.title}>{title}</AppText>
|
<AppText style={styles.title}>{title}</AppText>
|
||||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleCancel} style={styles.closeButton}>
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Закрыть"
|
||||||
|
onPress={handleCancel}
|
||||||
|
style={styles.closeButton}
|
||||||
|
{...closePressableProps}
|
||||||
|
>
|
||||||
<AppText style={styles.closeButtonText}>×</AppText>
|
<AppText style={styles.closeButtonText}>×</AppText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@ -116,6 +181,7 @@ function InputDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
|
ref={inputRef}
|
||||||
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
|
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChangeText={handleChangeText}
|
onChangeText={handleChangeText}
|
||||||
@ -123,9 +189,11 @@ function InputDialog({
|
|||||||
placeholderTextColor={styles.placeholder.color}
|
placeholderTextColor={styles.placeholder.color}
|
||||||
keyboardType={keyboardType}
|
keyboardType={keyboardType}
|
||||||
autoCapitalize={autoCapitalize}
|
autoCapitalize={autoCapitalize}
|
||||||
|
multiline={true}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
autoFocus={true}
|
autoFocus={Platform.OS !== 'android'}
|
||||||
|
focusable={Platform.OS !== 'android' || inputFocusable}
|
||||||
selectTextOnFocus={true}
|
selectTextOnFocus={true}
|
||||||
accessible={true}
|
accessible={true}
|
||||||
accessibilityLabel={label || placeholder}
|
accessibilityLabel={label || placeholder}
|
||||||
@ -140,11 +208,33 @@ function InputDialog({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.buttonsRow}>
|
<View style={styles.buttonsRow}>
|
||||||
<AppButton title={cancelText} onPress={handleCancel} style={styles.cancelButton} textStyle={styles.cancelButtonText} />
|
<AppButton
|
||||||
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} />
|
title={cancelText}
|
||||||
|
onPress={handleCancel}
|
||||||
|
style={styles.cancelButton}
|
||||||
|
textStyle={styles.cancelButtonText}
|
||||||
|
focusable={cancelButtonFocusable}
|
||||||
|
/>
|
||||||
|
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} focusable={confirmButtonFocusable} />
|
||||||
</View>
|
</View>
|
||||||
</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>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal transparent={true} animationType="fade" statusBarTranslucent={true} visible={true} onRequestClose={handleRequestClose}>
|
||||||
|
<View style={styles.backdrop}>{dialogContent}</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -153,4 +243,4 @@ function InputDialog({
|
|||||||
//Интерфейс модуля
|
//Интерфейс модуля
|
||||||
//----------------
|
//----------------
|
||||||
|
|
||||||
module.exports = InputDialog;
|
module.exports = React.forwardRef(InputDialog);
|
||||||
|
|||||||
@ -31,6 +31,8 @@ const PasswordInput = React.forwardRef(function PasswordInput(
|
|||||||
style,
|
style,
|
||||||
inputStyle,
|
inputStyle,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
|
onFocus: onFocusProp,
|
||||||
|
onBlur: onBlurProp,
|
||||||
...restProps
|
...restProps
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@ -50,15 +52,23 @@ const PasswordInput = React.forwardRef(function PasswordInput(
|
|||||||
[ref]
|
[ref]
|
||||||
);
|
);
|
||||||
|
|
||||||
//Обработчик фокуса
|
//Обработчик фокуса (внутренний + опциональный внешний)
|
||||||
const handleFocus = React.useCallback(() => {
|
const handleFocus = React.useCallback(
|
||||||
|
e => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
}, []);
|
if (typeof onFocusProp === 'function') onFocusProp(e);
|
||||||
|
},
|
||||||
|
[onFocusProp]
|
||||||
|
);
|
||||||
|
|
||||||
//Обработчик потери фокуса
|
//Обработчик потери фокуса (внутренний + опциональный внешний)
|
||||||
const handleBlur = React.useCallback(() => {
|
const handleBlur = React.useCallback(
|
||||||
|
e => {
|
||||||
setIsFocused(false);
|
setIsFocused(false);
|
||||||
}, []);
|
if (typeof onBlurProp === 'function') onBlurProp(e);
|
||||||
|
},
|
||||||
|
[onBlurProp]
|
||||||
|
);
|
||||||
|
|
||||||
//Обработчик переключения видимости пароля
|
//Обработчик переключения видимости пароля
|
||||||
const handleTogglePassword = React.useCallback(() => {
|
const handleTogglePassword = React.useCallback(() => {
|
||||||
|
|||||||
63
rn/app/src/components/layout/AppIdleProvider.js
Normal file
63
rn/app/src/components/layout/AppIdleProvider.js
Normal 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
|
||||||
|
};
|
||||||
@ -25,16 +25,12 @@ function AppLocalDbProvider({ children }) {
|
|||||||
const value = React.useMemo(
|
const value = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isDbReady: api.isDbReady,
|
isDbReady: api.isDbReady,
|
||||||
inspections: api.inspections,
|
|
||||||
error: api.error,
|
error: api.error,
|
||||||
loadInspections: api.loadInspections,
|
|
||||||
saveInspection: api.saveInspection,
|
|
||||||
getSetting: api.getSetting,
|
getSetting: api.getSetting,
|
||||||
setSetting: api.setSetting,
|
setSetting: api.setSetting,
|
||||||
deleteSetting: api.deleteSetting,
|
deleteSetting: api.deleteSetting,
|
||||||
getAllSettings: api.getAllSettings,
|
getAllSettings: api.getAllSettings,
|
||||||
clearSettings: api.clearSettings,
|
clearSettings: api.clearSettings,
|
||||||
clearInspections: api.clearInspections,
|
|
||||||
vacuum: api.vacuum,
|
vacuum: api.vacuum,
|
||||||
checkTableExists: api.checkTableExists,
|
checkTableExists: api.checkTableExists,
|
||||||
setAuthSession: api.setAuthSession,
|
setAuthSession: api.setAuthSession,
|
||||||
@ -43,16 +39,12 @@ function AppLocalDbProvider({ children }) {
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
api.isDbReady,
|
api.isDbReady,
|
||||||
api.inspections,
|
|
||||||
api.error,
|
api.error,
|
||||||
api.loadInspections,
|
|
||||||
api.saveInspection,
|
|
||||||
api.getSetting,
|
api.getSetting,
|
||||||
api.setSetting,
|
api.setSetting,
|
||||||
api.deleteSetting,
|
api.deleteSetting,
|
||||||
api.getAllSettings,
|
api.getAllSettings,
|
||||||
api.clearSettings,
|
api.clearSettings,
|
||||||
api.clearInspections,
|
|
||||||
api.vacuum,
|
api.vacuum,
|
||||||
api.checkTableExists,
|
api.checkTableExists,
|
||||||
api.setAuthSession,
|
api.setAuthSession,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const React = require('react'); //React и хуки
|
|||||||
const { useColorScheme, View } = require('react-native'); //Определение темы устройства и разметка
|
const { useColorScheme, View } = require('react-native'); //Определение темы устройства и разметка
|
||||||
const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
|
const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
|
||||||
const AppShell = require('./AppShell'); //Оболочка приложения
|
const AppShell = require('./AppShell'); //Оболочка приложения
|
||||||
|
const HardwareScannerProvider = require('./HardwareScannerProvider').HardwareScannerProvider; //Встроенный сканер
|
||||||
const styles = require('../../styles/layout/AppRoot.styles'); //Стили корневого layout
|
const styles = require('../../styles/layout/AppRoot.styles'); //Стили корневого layout
|
||||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||||
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
||||||
@ -54,7 +55,9 @@ function AppRoot() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
<HardwareScannerProvider>
|
||||||
<AppShell isDarkMode={isDarkMode} />
|
<AppShell isDarkMode={isDarkMode} />
|
||||||
|
</HardwareScannerProvider>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ const React = require('react'); //React
|
|||||||
const { StatusBar, Platform } = require('react-native'); //Базовые компоненты
|
const { StatusBar, Platform } = require('react-native'); //Базовые компоненты
|
||||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||||
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
||||||
|
const { useAppIdleContext } = require('./AppIdleProvider'); //Контекст простоя
|
||||||
const MainScreen = require('../../screens/MainScreen'); //Главный экран
|
const MainScreen = require('../../screens/MainScreen'); //Главный экран
|
||||||
const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
|
const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
|
||||||
const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации
|
const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации
|
||||||
@ -27,6 +28,12 @@ const styles = require('../../styles/layout/AppShell.styles'); //Стили об
|
|||||||
function AppShell({ isDarkMode }) {
|
function AppShell({ isDarkMode }) {
|
||||||
const { currentScreen, SCREENS } = useAppNavigationContext();
|
const { currentScreen, SCREENS } = useAppNavigationContext();
|
||||||
const { isStartupSessionCheckInProgress, isLogoutInProgress } = useAppAuthContext();
|
const { isStartupSessionCheckInProgress, isLogoutInProgress } = useAppAuthContext();
|
||||||
|
const { reportActivity } = useAppIdleContext();
|
||||||
|
|
||||||
|
//Сброс таймера простоя при смене экрана (навигация считается активностью)
|
||||||
|
React.useEffect(() => {
|
||||||
|
reportActivity();
|
||||||
|
}, [currentScreen, reportActivity]);
|
||||||
|
|
||||||
const renderScreen = React.useCallback(() => {
|
const renderScreen = React.useCallback(() => {
|
||||||
switch (currentScreen) {
|
switch (currentScreen) {
|
||||||
|
|||||||
97
rn/app/src/components/layout/HardwareScannerProvider.js
Normal file
97
rn/app/src/components/layout/HardwareScannerProvider.js
Normal 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
|
||||||
|
};
|
||||||
136
rn/app/src/components/menu/MenuItemIcon.js
Normal file
136
rn/app/src/components/menu/MenuItemIcon.js
Normal 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;
|
||||||
@ -10,25 +10,57 @@
|
|||||||
const React = require('react'); //React
|
const React = require('react'); //React
|
||||||
const { ScrollView, View } = require('react-native'); //Базовые компоненты
|
const { ScrollView, View } = require('react-native'); //Базовые компоненты
|
||||||
const MenuItem = require('./MenuItem'); //Элемент меню
|
const MenuItem = require('./MenuItem'); //Элемент меню
|
||||||
|
const MenuItemIcon = require('./MenuItemIcon'); //Иконка пункта меню
|
||||||
const EmptyMenu = require('./EmptyMenu'); //Пустое меню
|
const EmptyMenu = require('./EmptyMenu'); //Пустое меню
|
||||||
const MenuDivider = require('./MenuDivider'); //Разделитель меню
|
const MenuDivider = require('./MenuDivider'); //Разделитель меню
|
||||||
|
const { MENU_ITEM_ID_DIVIDER } = require('../../config/menuItemIds'); //ID разделителя
|
||||||
const styles = require('../../styles/menu/MenuList.styles'); //Стили списка меню
|
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 }) {
|
function MenuItemRow({ item, index, items, onItemPress }) {
|
||||||
const handlePress = React.useCallback(() => {
|
const handlePress = React.useCallback(() => {
|
||||||
onItemPress(item);
|
onItemPress(item);
|
||||||
}, [item, onItemPress]);
|
}, [item, onItemPress]);
|
||||||
|
|
||||||
|
const icon = React.useMemo(() => resolveItemIcon(item), [item]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
title={item.title}
|
title={item.title}
|
||||||
icon={item.icon}
|
icon={icon}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
isDestructive={item.isDestructive}
|
isDestructive={item.isDestructive}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
@ -54,21 +86,15 @@ function MenuList({ items = [], onClose, style }) {
|
|||||||
[onClose]
|
[onClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const listElements = React.useMemo(() => getMenuListElements(items, handleItemPress), [items, handleItemPress]);
|
||||||
|
|
||||||
if (!Array.isArray(items) || items.length === 0) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
return <EmptyMenu />;
|
return <EmptyMenu />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={[styles.scrollView, style]} showsVerticalScrollIndicator={false} bounces={false}>
|
<ScrollView style={[styles.scrollView, style]} showsVerticalScrollIndicator={false} bounces={false}>
|
||||||
{items.map((item, index) => {
|
{listElements}
|
||||||
//Элемент-разделитель
|
|
||||||
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} />;
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
const React = require('react'); //React и хуки
|
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 { Camera, useCameraDevice, useCodeScanner, useCameraPermission } = require('react-native-vision-camera'); //Камера и сканер кодов
|
||||||
const AppText = require('../common/AppText'); //Общий текст
|
const AppText = require('../common/AppText'); //Общий текст
|
||||||
const { DEFAULT_CODE_TYPES } = require('../../config/scannerConfig'); //Конфиг сканера
|
const { DEFAULT_CODE_TYPES } = require('../../config/scannerConfig'); //Конфиг сканера
|
||||||
@ -68,10 +68,12 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
|||||||
}
|
}
|
||||||
}, [hasPermission, requestPermission]);
|
}, [hasPermission, requestPermission]);
|
||||||
|
|
||||||
|
const noFocusProps = Platform.OS === 'android' ? { focusable: false } : {};
|
||||||
|
|
||||||
//Нет устройства камеры
|
//Нет устройства камеры
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.fallbackContainer}>
|
<View style={styles.fallbackContainer} {...noFocusProps}>
|
||||||
<AppText style={styles.fallbackText} variant="body">
|
<AppText style={styles.fallbackText} variant="body">
|
||||||
{NO_CAMERA_MESSAGE}
|
{NO_CAMERA_MESSAGE}
|
||||||
</AppText>
|
</AppText>
|
||||||
@ -82,7 +84,7 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
|||||||
//Нет разрешения на камеру
|
//Нет разрешения на камеру
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.fallbackContainer}>
|
<View style={styles.fallbackContainer} {...noFocusProps}>
|
||||||
<AppText style={styles.fallbackText} variant="body">
|
<AppText style={styles.fallbackText} variant="body">
|
||||||
{PERMISSION_DENIED_MESSAGE}
|
{PERMISSION_DENIED_MESSAGE}
|
||||||
</AppText>
|
</AppText>
|
||||||
@ -90,8 +92,10 @@ function BarcodeScannerNative({ onScan, isActive = true }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerProps = Platform.OS === 'android' ? { focusable: false, importantForAccessibility: 'no-hide-descendants' } : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container} {...containerProps}>
|
||||||
<Camera style={styles.camera} device={device} isActive={isActive} codeScanner={codeScanner} />
|
<Camera style={styles.camera} device={device} isActive={isActive} codeScanner={codeScanner} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
34
rn/app/src/components/scanner/CameraPausedOverlay.js
Normal file
34
rn/app/src/components/scanner/CameraPausedOverlay.js
Normal 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;
|
||||||
@ -8,10 +8,11 @@
|
|||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
const React = require('react'); //React
|
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 AppText = require('../common/AppText'); //Общий текстовый компонент
|
||||||
const AppButton = require('../common/AppButton'); //Кнопка
|
const AppButton = require('../common/AppButton'); //Кнопка
|
||||||
const { SCAN_RESULT_MODAL_TITLE, SCAN_RESULT_CLOSE_BUTTON } = require('../../config/messages'); //Сообщения
|
const { SCAN_RESULT_MODAL_TITLE, SCAN_RESULT_CLOSE_BUTTON } = require('../../config/messages'); //Сообщения
|
||||||
|
const { noop } = require('../../utils/noop'); //Пустая функция
|
||||||
const styles = require('../../styles/scanner/ScanResultModal.styles'); //Стили модального окна
|
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 }) {
|
function ScanResultModal({ visible, codeType, value, onRequestClose }) {
|
||||||
const handleClosePress = React.useCallback(() => {
|
const handleClosePress = React.useCallback(() => {
|
||||||
handleClose(onRequestClose);
|
handleClose(onRequestClose);
|
||||||
}, [onRequestClose]);
|
}, [onRequestClose]);
|
||||||
|
|
||||||
|
const modalOnRequestClose = React.useMemo(() => (Platform.OS === 'android' ? noop : handleClosePress), [handleClosePress]);
|
||||||
|
|
||||||
const displayType = formatCodeType(codeType);
|
const displayType = formatCodeType(codeType);
|
||||||
const displayValue = value != null && value !== '' ? String(value) : '—';
|
const displayValue = value != null && value !== '' ? String(value) : '—';
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={handleClosePress}>
|
<View style={styles.overlay} focusable={false} importantForAccessibility="no-hide-descendants" collapsable={false}>
|
||||||
<View style={styles.backdrop}>
|
<View style={styles.overlayContent} pointerEvents="box-none">
|
||||||
<View style={styles.container}>
|
{content}
|
||||||
<View style={styles.header}>
|
|
||||||
<AppText style={styles.title} numberOfLines={1}>
|
|
||||||
{SCAN_RESULT_MODAL_TITLE}
|
|
||||||
</AppText>
|
|
||||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть" onPress={handleClosePress} style={styles.closeButton}>
|
|
||||||
<AppText style={styles.closeButtonText}>×</AppText>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<View style={styles.content}>
|
|
||||||
<AppText style={styles.typeLabel} variant="caption" weight="medium">
|
|
||||||
Тип: {displayType}
|
|
||||||
</AppText>
|
|
||||||
<View style={styles.valueBlock}>
|
|
||||||
<AppText style={styles.valueText} selectable={true}>
|
|
||||||
{displayValue}
|
|
||||||
</AppText>
|
|
||||||
</View>
|
|
||||||
<View style={styles.buttonsRow}>
|
|
||||||
<AppButton title={SCAN_RESULT_CLOSE_BUTTON} onPress={handleClosePress} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={modalOnRequestClose} statusBarTranslucent={true}>
|
||||||
|
<View style={styles.modalRoot}>{content}</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,10 +8,12 @@
|
|||||||
//---------------------
|
//---------------------
|
||||||
|
|
||||||
const React = require('react'); //React
|
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 BarcodeScanner = require('./BarcodeScanner'); //Сканер
|
||||||
const ScannerPlaceholder = require('./ScannerPlaceholder'); //Заглушка с кнопкой
|
const ScannerPlaceholder = require('./ScannerPlaceholder'); //Заглушка с кнопкой
|
||||||
|
const CameraPausedOverlay = require('./CameraPausedOverlay'); //Оверлей «камера на паузе»
|
||||||
const AppText = require('../common/AppText'); //Текст
|
const AppText = require('../common/AppText'); //Текст
|
||||||
|
const { SCANNER_CAMERA_TAP_TO_RESUME } = require('../../config/messages'); //Сообщения
|
||||||
const styles = require('../../styles/scanner/ScannerArea.styles'); //Стили области
|
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 [scannerModalVisible, setScannerModalVisible] = React.useState(false);
|
||||||
|
|
||||||
const handleScanFromArea = React.useCallback(
|
const handleScanFromArea = React.useCallback(
|
||||||
@ -56,9 +67,33 @@ function ScannerArea({ alwaysShowScanner = false, scannerOpen = true, onScanResu
|
|||||||
|
|
||||||
const renderScannerContent = () => {
|
const renderScannerContent = () => {
|
||||||
if (alwaysShowScanner && scannerOpen) {
|
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 (
|
return (
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
const React = require('react'); //React
|
const React = require('react'); //React
|
||||||
const { View } = require('react-native'); //Базовые компоненты
|
const { View } = require('react-native'); //Базовые компоненты
|
||||||
const AppButton = require('../common/AppButton'); //Кнопка
|
const AppButton = require('../common/AppButton'); //Кнопка
|
||||||
|
const AppText = require('../common/AppText'); //Текст
|
||||||
const { SCAN_BUTTON_TITLE } = require('../../config/messages'); //Сообщения
|
const { SCAN_BUTTON_TITLE } = require('../../config/messages'); //Сообщения
|
||||||
const styles = require('../../styles/scanner/ScannerPlaceholder.styles'); //Стили заглушки
|
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(() => {
|
const handlePress = React.useCallback(() => {
|
||||||
if (typeof onScanPress === 'function') {
|
if (typeof onScanPress === 'function') {
|
||||||
onScanPress();
|
onScanPress();
|
||||||
}
|
}
|
||||||
}, [onScanPress]);
|
}, [onScanPress]);
|
||||||
|
|
||||||
|
const handleSecondary = React.useCallback(() => {
|
||||||
|
if (typeof secondaryButtonOnPress === 'function') {
|
||||||
|
secondaryButtonOnPress();
|
||||||
|
}
|
||||||
|
}, [secondaryButtonOnPress]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<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} />
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const UI = {
|
|||||||
|
|
||||||
//Высоты элементов
|
//Высоты элементов
|
||||||
BUTTON_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
|
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)
|
HEADER_HEIGHT: responsiveSize(isTablet() ? 80 : Platform.OS === 'ios' ? 70 : 56)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -13,16 +13,18 @@ const AUTH_SETTINGS_KEYS = {
|
|||||||
LAST_CONNECTED_SERVER_URL: 'auth_last_connected_server_url',
|
LAST_CONNECTED_SERVER_URL: 'auth_last_connected_server_url',
|
||||||
HIDE_SERVER_URL: 'auth_hide_server_url',
|
HIDE_SERVER_URL: 'auth_hide_server_url',
|
||||||
IDLE_TIMEOUT: 'auth_idle_timeout',
|
IDLE_TIMEOUT: 'auth_idle_timeout',
|
||||||
|
SERVER_REQUEST_TIMEOUT: 'auth_server_request_timeout',
|
||||||
DEVICE_ID: 'auth_device_id',
|
DEVICE_ID: 'auth_device_id',
|
||||||
DEVICE_SECRET_KEY: 'auth_device_secret_key',
|
DEVICE_SECRET_KEY: 'auth_device_secret_key',
|
||||||
SAVED_LOGIN: 'auth_saved_login',
|
SAVED_LOGIN: 'auth_saved_login',
|
||||||
SAVED_PASSWORD: 'auth_saved_password',
|
SAVED_PASSWORD: 'auth_saved_password',
|
||||||
SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled',
|
SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled',
|
||||||
ALWAYS_SHOW_SCANNER: 'main_always_show_scanner'
|
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 = [
|
const CONNECTION_SETTINGS_KEYS = [
|
||||||
@ -42,5 +44,5 @@ const CONNECTION_SETTINGS_KEYS = [
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
AUTH_SETTINGS_KEYS,
|
AUTH_SETTINGS_KEYS,
|
||||||
CONNECTION_SETTINGS_KEYS,
|
CONNECTION_SETTINGS_KEYS,
|
||||||
DEFAULT_IDLE_TIMEOUT
|
DEFAULT_SERVER_REQUEST_TIMEOUT
|
||||||
};
|
};
|
||||||
|
|||||||
27
rn/app/src/config/authFormFieldsConfig.js
Normal file
27
rn/app/src/config/authFormFieldsConfig.js
Normal 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
|
||||||
|
};
|
||||||
19
rn/app/src/config/inputDeviceConfig.js
Normal file
19
rn/app/src/config/inputDeviceConfig.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Предрейсовые осмотры - мобильное приложение
|
||||||
|
Конфигурация совместной работы устройств ввода (клавиатура, встроенный сканер)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//---------
|
||||||
|
//Константы
|
||||||
|
//---------
|
||||||
|
|
||||||
|
//Режим подстановки данных сканера в поле: всегда замена текущего значения
|
||||||
|
const SCAN_INPUT_MODE_REPLACE = 'replace';
|
||||||
|
|
||||||
|
//----------------
|
||||||
|
//Интерфейс модуля
|
||||||
|
//----------------
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SCAN_INPUT_MODE_REPLACE
|
||||||
|
};
|
||||||
35
rn/app/src/config/menuItemIds.js
Normal file
35
rn/app/src/config/menuItemIds.js
Normal 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
|
||||||
|
};
|
||||||
@ -13,6 +13,9 @@ const CONNECTION_LOST_MESSAGE = 'Нет связи с сервером. Прил
|
|||||||
//Заголовок сообщения при переходе в режим офлайн
|
//Заголовок сообщения при переходе в режим офлайн
|
||||||
const OFFLINE_MODE_TITLE = 'Режим офлайн';
|
const OFFLINE_MODE_TITLE = 'Режим офлайн';
|
||||||
|
|
||||||
|
//Сообщение при переходе в офлайн по таймауту простоя
|
||||||
|
const IDLE_TIMEOUT_OFFLINE_MESSAGE = 'Сессия закрыта по таймауту простоя. Приложение переведено в режим офлайн.';
|
||||||
|
|
||||||
//Сообщение при проверке соединения при старте приложения
|
//Сообщение при проверке соединения при старте приложения
|
||||||
const STARTUP_CHECK_CONNECTION_MESSAGE = 'Проверка соединения...';
|
const STARTUP_CHECK_CONNECTION_MESSAGE = 'Проверка соединения...';
|
||||||
|
|
||||||
@ -35,7 +38,10 @@ const MENU_ITEM_LOGOUT = 'Выход';
|
|||||||
const AUTH_SCREEN_TITLE = 'Вход в приложение';
|
const AUTH_SCREEN_TITLE = 'Вход в приложение';
|
||||||
const AUTH_BUTTON_LOGIN = 'Войти';
|
const AUTH_BUTTON_LOGIN = 'Войти';
|
||||||
const AUTH_BUTTON_LOADING = 'Вход...';
|
const AUTH_BUTTON_LOADING = 'Вход...';
|
||||||
|
const AUTH_BUTTON_LOGOUT = 'Выйти';
|
||||||
const LOGOUT_IN_PROGRESS_MESSAGE = 'Выход...';
|
const LOGOUT_IN_PROGRESS_MESSAGE = 'Выход...';
|
||||||
|
const LOGOUT_CONFIRM_TITLE = 'Подтверждение выхода';
|
||||||
|
const LOGOUT_CONFIRM_MESSAGE = 'Вы уверены, что хотите выйти?';
|
||||||
|
|
||||||
//Диалог подтверждения при смене сервера (относительно последнего успешного подключения)
|
//Диалог подтверждения при смене сервера (относительно последнего успешного подключения)
|
||||||
const AUTH_SERVER_CHANGE_CONFIRM_TITLE = 'Подтверждение входа';
|
const AUTH_SERVER_CHANGE_CONFIRM_TITLE = 'Подтверждение входа';
|
||||||
@ -52,10 +58,16 @@ const SCREEN_TITLE_SETTINGS = 'Настройки';
|
|||||||
|
|
||||||
//Сканер на главном экране
|
//Сканер на главном экране
|
||||||
const SCANNER_SETTING_LABEL = 'Всегда отображать сканер на главном экране';
|
const SCANNER_SETTING_LABEL = 'Всегда отображать сканер на главном экране';
|
||||||
|
const SCANNER_PRIORITY_LABEL = 'Приоритет на главном экране';
|
||||||
|
const SCANNER_PRIORITY_CAMERA = 'Камера';
|
||||||
|
const SCANNER_PRIORITY_HARDWARE = 'Встроенный сканер';
|
||||||
const SCAN_BUTTON_TITLE = 'Сканировать';
|
const SCAN_BUTTON_TITLE = 'Сканировать';
|
||||||
const SCAN_RESULT_MODAL_TITLE = 'Результат сканирования';
|
const SCAN_RESULT_MODAL_TITLE = 'Результат сканирования';
|
||||||
const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
|
const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
|
||||||
|
|
||||||
|
//Оверлей паузы камеры на главном экране: подсказка включить камеру по тапу
|
||||||
|
const SCANNER_CAMERA_TAP_TO_RESUME = 'Нажмите на область камеры для её включения';
|
||||||
|
|
||||||
//----------------
|
//----------------
|
||||||
//Интерфейс модуля
|
//Интерфейс модуля
|
||||||
//----------------
|
//----------------
|
||||||
@ -63,6 +75,7 @@ const SCAN_RESULT_CLOSE_BUTTON = 'Закрыть';
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
CONNECTION_LOST_MESSAGE,
|
CONNECTION_LOST_MESSAGE,
|
||||||
OFFLINE_MODE_TITLE,
|
OFFLINE_MODE_TITLE,
|
||||||
|
IDLE_TIMEOUT_OFFLINE_MESSAGE,
|
||||||
STARTUP_CHECK_CONNECTION_MESSAGE,
|
STARTUP_CHECK_CONNECTION_MESSAGE,
|
||||||
APP_ABOUT_TITLE,
|
APP_ABOUT_TITLE,
|
||||||
SIDE_MENU_TITLE,
|
SIDE_MENU_TITLE,
|
||||||
@ -74,7 +87,10 @@ module.exports = {
|
|||||||
AUTH_SCREEN_TITLE,
|
AUTH_SCREEN_TITLE,
|
||||||
AUTH_BUTTON_LOGIN,
|
AUTH_BUTTON_LOGIN,
|
||||||
AUTH_BUTTON_LOADING,
|
AUTH_BUTTON_LOADING,
|
||||||
|
AUTH_BUTTON_LOGOUT,
|
||||||
LOGOUT_IN_PROGRESS_MESSAGE,
|
LOGOUT_IN_PROGRESS_MESSAGE,
|
||||||
|
LOGOUT_CONFIRM_TITLE,
|
||||||
|
LOGOUT_CONFIRM_MESSAGE,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||||
@ -83,7 +99,11 @@ module.exports = {
|
|||||||
SETTINGS_RESET_SUCCESS_MESSAGE,
|
SETTINGS_RESET_SUCCESS_MESSAGE,
|
||||||
SCREEN_TITLE_SETTINGS,
|
SCREEN_TITLE_SETTINGS,
|
||||||
SCANNER_SETTING_LABEL,
|
SCANNER_SETTING_LABEL,
|
||||||
|
SCANNER_PRIORITY_LABEL,
|
||||||
|
SCANNER_PRIORITY_CAMERA,
|
||||||
|
SCANNER_PRIORITY_HARDWARE,
|
||||||
SCAN_BUTTON_TITLE,
|
SCAN_BUTTON_TITLE,
|
||||||
SCAN_RESULT_MODAL_TITLE,
|
SCAN_RESULT_MODAL_TITLE,
|
||||||
SCAN_RESULT_CLOSE_BUTTON
|
SCAN_RESULT_CLOSE_BUTTON,
|
||||||
|
SCANNER_CAMERA_TAP_TO_RESUME
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,8 +10,24 @@
|
|||||||
//Типы кодов для распознавания: QR и распространённые штрихкоды
|
//Типы кодов для распознавания: QR и распространённые штрихкоды
|
||||||
const DEFAULT_CODE_TYPES = ['qr', 'code-128', 'code-39', 'ean-13', 'ean-8', 'upc-a', 'upc-e', 'pdf-417', 'aztec', 'data-matrix'];
|
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);
|
return Boolean(alwaysShowScanner && !hasScanResult && !isStartupCheckInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,5 +37,12 @@ function shouldShowScanner({ alwaysShowScanner, hasScanResult, isStartupCheckInP
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
DEFAULT_CODE_TYPES,
|
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
|
shouldShowScanner
|
||||||
};
|
};
|
||||||
|
|||||||
@ -44,7 +44,6 @@ class SQLiteDatabase {
|
|||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
return this.db;
|
return this.db;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка инициализации базы данных:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,15 +58,8 @@ class SQLiteDatabase {
|
|||||||
//Выполняем SQL запросы последовательно
|
//Выполняем SQL запросы последовательно
|
||||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_APP_SETTINGS);
|
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_TABLE_AUTH_SESSION);
|
||||||
|
|
||||||
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS);
|
|
||||||
|
|
||||||
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_CREATED);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка настройки базы данных:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +74,6 @@ class SQLiteDatabase {
|
|||||||
const result = await this.db.executeAsync(sql, params);
|
const result = await this.db.executeAsync(sql, params);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка выполнения SQL запроса:', error, 'SQL:', sql, 'Params:', params);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +88,6 @@ class SQLiteDatabase {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения настройки:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +99,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.SETTINGS_SET, [key, stringValue]);
|
await this.executeQuery(this.sqlQueries.SETTINGS_SET, [key, stringValue]);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения настройки:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +109,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.SETTINGS_DELETE, [key]);
|
await this.executeQuery(this.sqlQueries.SETTINGS_DELETE, [key]);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка удаления настройки:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,7 +132,6 @@ class SQLiteDatabase {
|
|||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения всех настроек:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,126 +142,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.SETTINGS_CLEAR_ALL, []);
|
await this.executeQuery(this.sqlQueries.SETTINGS_CLEAR_ALL, []);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,7 +152,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.UTILITY_VACUUM, []);
|
await this.executeQuery(this.sqlQueries.UTILITY_VACUUM, []);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка оптимизации базы данных:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,7 +182,6 @@ class SQLiteDatabase {
|
|||||||
]);
|
]);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения сессии авторизации:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,7 +208,6 @@ class SQLiteDatabase {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения сессии авторизации:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,7 +218,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.AUTH_SESSION_CLEAR, []);
|
await this.executeQuery(this.sqlQueries.AUTH_SESSION_CLEAR, []);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка очистки сессии авторизации:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,7 +228,6 @@ class SQLiteDatabase {
|
|||||||
const result = await this.executeQuery(this.sqlQueries.UTILITY_CHECK_TABLE, [tableName]);
|
const result = await this.executeQuery(this.sqlQueries.UTILITY_CHECK_TABLE, [tableName]);
|
||||||
return result.rows && result.rows.length > 0 && result.rows.item(0).exists === 1;
|
return result.rows && result.rows.length > 0 && result.rows.item(0).exists === 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка проверки существования таблицы:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,7 +238,6 @@ class SQLiteDatabase {
|
|||||||
await this.executeQuery(this.sqlQueries.UTILITY_DROP_TABLE, [tableName]);
|
await this.executeQuery(this.sqlQueries.UTILITY_DROP_TABLE, [tableName]);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка удаления таблицы:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,7 +252,6 @@ class SQLiteDatabase {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка закрытия базы данных:', error);
|
|
||||||
this.db = null;
|
this.db = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -9,13 +9,8 @@
|
|||||||
|
|
||||||
//Таблицы
|
//Таблицы
|
||||||
const CREATE_TABLE_APP_SETTINGS = require('./settings/create_table_app_settings.sql');
|
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_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_GET = require('./settings/get_setting.sql');
|
||||||
const SETTINGS_SET = require('./settings/set_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_CLEAR_ALL = require('./settings/clear_all_settings.sql');
|
||||||
const SETTINGS_GET_ALL = require('./settings/get_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_SET = require('./auth/set_auth_session.sql');
|
||||||
const AUTH_SESSION_GET = require('./auth/get_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 запросов в один объект
|
//Сбор всех SQL запросов в один объект
|
||||||
const SQLQueries = {
|
const SQLQueries = {
|
||||||
CREATE_TABLE_APP_SETTINGS,
|
CREATE_TABLE_APP_SETTINGS,
|
||||||
CREATE_TABLE_INSPECTIONS,
|
|
||||||
CREATE_TABLE_AUTH_SESSION,
|
CREATE_TABLE_AUTH_SESSION,
|
||||||
CREATE_INDEX_INSPECTIONS_STATUS,
|
|
||||||
CREATE_INDEX_INSPECTIONS_CREATED,
|
|
||||||
SETTINGS_GET,
|
SETTINGS_GET,
|
||||||
SETTINGS_SET,
|
SETTINGS_SET,
|
||||||
SETTINGS_DELETE,
|
SETTINGS_DELETE,
|
||||||
SETTINGS_CLEAR_ALL,
|
SETTINGS_CLEAR_ALL,
|
||||||
SETTINGS_GET_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_SET,
|
||||||
AUTH_SESSION_GET,
|
AUTH_SESSION_GET,
|
||||||
AUTH_SESSION_CLEAR,
|
AUTH_SESSION_CLEAR,
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: подсчет количества осмотров
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_COUNT = `
|
|
||||||
-- Подсчет количества осмотров
|
|
||||||
SELECT COUNT(*) as count FROM inspections;
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_COUNT;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: удаление всех осмотров
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_DELETE_ALL = `
|
|
||||||
-- Удаление всех осмотров
|
|
||||||
DELETE FROM inspections;
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_DELETE_ALL;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: удаление осмотра
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_DELETE = `
|
|
||||||
-- Удаление осмотра по ID
|
|
||||||
DELETE FROM inspections WHERE id = ?;
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_DELETE;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: получение всех осмотров
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_GET_ALL = `
|
|
||||||
-- Получение всех осмотров
|
|
||||||
SELECT * FROM inspections ORDER BY created_at DESC;
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_GET_ALL;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: получение осмотра по ID
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_GET_BY_ID = `
|
|
||||||
-- Получение осмотра по ID
|
|
||||||
SELECT * FROM inspections WHERE id = ?;
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_GET_BY_ID;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
/*
|
|
||||||
Предрейсовые осмотры - мобильное приложение
|
|
||||||
SQL запрос: вставка нового осмотра
|
|
||||||
*/
|
|
||||||
|
|
||||||
//-----------
|
|
||||||
//Тело модуля
|
|
||||||
//-----------
|
|
||||||
|
|
||||||
const INSPECTIONS_INSERT = `
|
|
||||||
-- Вставка нового осмотра
|
|
||||||
INSERT INTO inspections (id, title, status, created_at, data)
|
|
||||||
VALUES (?, ?, ?, ?, ?);
|
|
||||||
`;
|
|
||||||
|
|
||||||
//----------------
|
|
||||||
//Интерфейс модуля
|
|
||||||
//----------------
|
|
||||||
|
|
||||||
module.exports = INSPECTIONS_INSERT;
|
|
||||||
@ -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;
|
|
||||||
@ -52,7 +52,6 @@ function useAppMode() {
|
|||||||
}
|
}
|
||||||
//При отсутствии сохранённого режима остаётся NOT_CONNECTED
|
//При отсутствии сохранённого режима остаётся NOT_CONNECTED
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки режима:', error);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
@ -71,9 +70,7 @@ function useAppMode() {
|
|||||||
const saveMode = async () => {
|
const saveMode = async () => {
|
||||||
try {
|
try {
|
||||||
await setSetting(STORAGE_KEY, mode);
|
await setSetting(STORAGE_KEY, mode);
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
console.error('Ошибка сохранения режима:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
saveMode();
|
saveMode();
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
const React = require('react'); //React и хуки
|
const React = require('react'); //React и хуки
|
||||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||||
|
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
|
||||||
|
|
||||||
//---------
|
//---------
|
||||||
//Константы
|
//Константы
|
||||||
@ -60,9 +61,7 @@ function useAppServer() {
|
|||||||
let baseURL = '';
|
let baseURL = '';
|
||||||
try {
|
try {
|
||||||
baseURL = (await getSetting('app_server_url')) || '';
|
baseURL = (await getSetting('app_server_url')) || '';
|
||||||
} catch (e) {
|
} catch (_e) {}
|
||||||
console.warn('Не удалось прочитать настройки сервера:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!baseURL) {
|
if (!baseURL) {
|
||||||
return makeRespErr({
|
return makeRespErr({
|
||||||
@ -77,6 +76,14 @@ function useAppServer() {
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
|
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
|
||||||
|
let timeoutId = null;
|
||||||
|
if (timeoutMs > 0) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
let response = null;
|
let response = null;
|
||||||
let responseJSON = null;
|
let responseJSON = null;
|
||||||
|
|
||||||
@ -97,6 +104,9 @@ function useAppServer() {
|
|||||||
message: `${ERR_NETWORK}: ${e.message || 'неопределённая ошибка'}`
|
message: `${ERR_NETWORK}: ${e.message || 'неопределённая ошибка'}`
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
if (timeoutId != null) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,11 @@
|
|||||||
|
|
||||||
const React = require('react'); //React и хуки
|
const React = require('react'); //React и хуки
|
||||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
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 { ACTION_CODES, RESPONSE_STATES, ERROR_MESSAGES } = require('../config/authApi'); //API авторизации
|
||||||
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage'); //Шифрование
|
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage'); //Шифрование
|
||||||
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId'); //Идентификатор устройства
|
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId'); //Идентификатор устройства
|
||||||
|
const { getServerRequestTimeoutMs } = require('../utils/serverRequestTimeout'); //Таймаут запроса к серверу
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
//Тело модуля
|
//Тело модуля
|
||||||
@ -79,7 +80,8 @@ function useAuth() {
|
|||||||
}, [getSetting, setSetting]);
|
}, [getSetting, setSetting]);
|
||||||
|
|
||||||
//Выполнение запроса к серверу
|
//Выполнение запроса к серверу
|
||||||
const executeRequest = React.useCallback(async (serverUrl, payload) => {
|
const executeRequest = React.useCallback(
|
||||||
|
async (serverUrl, payload) => {
|
||||||
//Отменяем предыдущий запрос если есть
|
//Отменяем предыдущий запрос если есть
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
abortControllerRef.current.abort();
|
||||||
@ -88,7 +90,15 @@ function useAuth() {
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
|
let timeoutId = null;
|
||||||
try {
|
try {
|
||||||
|
const timeoutMs = await getServerRequestTimeoutMs(getSetting);
|
||||||
|
if (timeoutMs > 0) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(serverUrl, {
|
const response = await fetch(serverUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -111,9 +121,14 @@ function useAuth() {
|
|||||||
|
|
||||||
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (timeoutId != null) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[getSetting]
|
||||||
|
);
|
||||||
|
|
||||||
//Сохранение credentials после успешной аутентификации
|
//Сохранение credentials после успешной аутентификации
|
||||||
const saveCredentials = React.useCallback(
|
const saveCredentials = React.useCallback(
|
||||||
@ -131,7 +146,6 @@ function useAuth() {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
console.error('Ошибка сохранения credentials:', saveError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -182,7 +196,6 @@ function useAuth() {
|
|||||||
savePasswordEnabled: true
|
savePasswordEnabled: true
|
||||||
};
|
};
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
console.error('Ошибка загрузки credentials:', loadError);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [getSetting, getDeviceId, getSecretKey]);
|
}, [getSetting, getDeviceId, getSecretKey]);
|
||||||
@ -195,7 +208,6 @@ function useAuth() {
|
|||||||
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
|
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
|
||||||
return true;
|
return true;
|
||||||
} catch (clearError) {
|
} catch (clearError) {
|
||||||
console.error('Ошибка очистки credentials:', clearError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [setSetting]);
|
}, [setSetting]);
|
||||||
@ -229,8 +241,8 @@ function useAuth() {
|
|||||||
requestPayload.XREQUEST.XPAYLOAD.SCOMPANY = company;
|
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;
|
requestPayload.XREQUEST.XPAYLOAD.NTIMEOUT = timeoutValue;
|
||||||
|
|
||||||
//Выполняем запрос
|
//Выполняем запрос
|
||||||
@ -405,16 +417,18 @@ function useAuth() {
|
|||||||
//Выход из системы
|
//Выход из системы
|
||||||
const logout = React.useCallback(
|
const logout = React.useCallback(
|
||||||
async (options = {}) => {
|
async (options = {}) => {
|
||||||
const { skipServerRequest = false } = options;
|
const { skipServerRequest = false, keepSessionLocally = false } = options;
|
||||||
|
|
||||||
|
if (!keepSessionLocally) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsLogoutInProgress(true);
|
setIsLogoutInProgress(true);
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentSession = session || (await getAuthSession());
|
const currentSession = session || (await getAuthSession());
|
||||||
|
|
||||||
//Запрос на сервер только при онлайн и если не отключено опцией
|
//Запрос на сервер при наличии сессии и если не отключено опцией
|
||||||
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
|
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
XREQUEST: {
|
XREQUEST: {
|
||||||
@ -429,14 +443,14 @@ function useAuth() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await executeRequest(currentSession.serverUrl, requestPayload);
|
await executeRequest(currentSession.serverUrl, requestPayload);
|
||||||
} catch (logoutError) {
|
} catch (_logoutError) {}
|
||||||
console.warn('Ошибка при выходе из системы:', logoutError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!keepSessionLocally) {
|
||||||
await clearAuthSession();
|
await clearAuthSession();
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (logoutError) {
|
} catch (logoutError) {
|
||||||
@ -444,9 +458,11 @@ function useAuth() {
|
|||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
} finally {
|
} finally {
|
||||||
|
if (!keepSessionLocally) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsLogoutInProgress(false);
|
setIsLogoutInProgress(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[session, getAuthSession, executeRequest, clearAuthSession]
|
[session, getAuthSession, executeRequest, clearAuthSession]
|
||||||
);
|
);
|
||||||
@ -580,8 +596,7 @@ function useAuth() {
|
|||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
sessionRestoredFromStorageRef.current = true;
|
sessionRestoredFromStorageRef.current = true;
|
||||||
}
|
}
|
||||||
} catch (initError) {
|
} catch (_initError) {
|
||||||
console.error('Ошибка инициализации авторизации:', initError);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
}
|
}
|
||||||
|
|||||||
121
rn/app/src/hooks/useIdleTimeout.js
Normal file
121
rn/app/src/hooks/useIdleTimeout.js
Normal 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;
|
||||||
@ -17,7 +17,6 @@ const SQLiteDatabase = require('../database/SQLiteDatabase'); //Модуль SQL
|
|||||||
function useLocalDb() {
|
function useLocalDb() {
|
||||||
//Состояние хука
|
//Состояние хука
|
||||||
const [isDbReady, setIsDbReady] = React.useState(false);
|
const [isDbReady, setIsDbReady] = React.useState(false);
|
||||||
const [inspections, setInspections] = React.useState([]);
|
|
||||||
const [dbError, setDbError] = React.useState(null);
|
const [dbError, setDbError] = React.useState(null);
|
||||||
|
|
||||||
//Отслеживания статуса инициализации
|
//Отслеживания статуса инициализации
|
||||||
@ -33,12 +32,6 @@ function useLocalDb() {
|
|||||||
if (initializingRef.current || SQLiteDatabase.isInitialized) {
|
if (initializingRef.current || SQLiteDatabase.isInitialized) {
|
||||||
if (mounted && SQLiteDatabase.isInitialized) {
|
if (mounted && SQLiteDatabase.isInitialized) {
|
||||||
setIsDbReady(true);
|
setIsDbReady(true);
|
||||||
try {
|
|
||||||
const loadedInspections = await SQLiteDatabase.getInspections();
|
|
||||||
setInspections(loadedInspections);
|
|
||||||
} catch (loadError) {
|
|
||||||
console.error('Ошибка загрузки осмотров при переинициализации:', loadError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -49,12 +42,9 @@ function useLocalDb() {
|
|||||||
await SQLiteDatabase.initialize();
|
await SQLiteDatabase.initialize();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setIsDbReady(true);
|
setIsDbReady(true);
|
||||||
const loadedInspections = await SQLiteDatabase.getInspections();
|
|
||||||
setInspections(loadedInspections);
|
|
||||||
setDbError(null);
|
setDbError(null);
|
||||||
}
|
}
|
||||||
} catch (initError) {
|
} catch (_initError) {
|
||||||
console.error('Ошибка инициализации базы данных:', initError);
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setIsDbReady(false);
|
setIsDbReady(false);
|
||||||
setDbError('Не удалось инициализировать базу данных');
|
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(
|
const getSetting = React.useCallback(
|
||||||
async key => {
|
async key => {
|
||||||
@ -122,8 +70,7 @@ function useLocalDb() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.getSetting(key);
|
return await SQLiteDatabase.getSetting(key);
|
||||||
} catch (getSettingError) {
|
} catch (_getSettingError) {
|
||||||
console.error('Ошибка получения настройки:', getSettingError);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -133,15 +80,11 @@ function useLocalDb() {
|
|||||||
//Сохранение настройки
|
//Сохранение настройки
|
||||||
const setSetting = React.useCallback(
|
const setSetting = React.useCallback(
|
||||||
async (key, value) => {
|
async (key, value) => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.setSetting(key, value);
|
return await SQLiteDatabase.setSetting(key, value);
|
||||||
} catch (setSettingError) {
|
} catch (_setSettingError) {
|
||||||
console.error('Ошибка сохранения настройки:', setSettingError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -151,15 +94,11 @@ function useLocalDb() {
|
|||||||
//Удаление настройки
|
//Удаление настройки
|
||||||
const deleteSetting = React.useCallback(
|
const deleteSetting = React.useCallback(
|
||||||
async key => {
|
async key => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.deleteSetting(key);
|
return await SQLiteDatabase.deleteSetting(key);
|
||||||
} catch (deleteSettingError) {
|
} catch (_deleteSettingError) {
|
||||||
console.error('Ошибка удаления настройки:', deleteSettingError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -168,66 +107,33 @@ function useLocalDb() {
|
|||||||
|
|
||||||
//Получение всех настроек
|
//Получение всех настроек
|
||||||
const getAllSettings = React.useCallback(async () => {
|
const getAllSettings = React.useCallback(async () => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return {};
|
||||||
console.warn('База данных не готова');
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.getAllSettings();
|
return await SQLiteDatabase.getAllSettings();
|
||||||
} catch (getAllSettingsError) {
|
} catch (_getAllSettingsError) {
|
||||||
console.error('Ошибка получения всех настроек:', getAllSettingsError);
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}, [isDbReady]);
|
}, [isDbReady]);
|
||||||
|
|
||||||
//Очистка всех настроек
|
//Очистка всех настроек
|
||||||
const clearSettings = React.useCallback(async () => {
|
const clearSettings = React.useCallback(async () => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.clearSettings();
|
return await SQLiteDatabase.clearSettings();
|
||||||
} catch (clearSettingsError) {
|
} 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('Не удалось очистить осмотры');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [isDbReady]);
|
}, [isDbReady]);
|
||||||
|
|
||||||
//Оптимизация базы данных
|
//Оптимизация базы данных
|
||||||
const vacuum = React.useCallback(async () => {
|
const vacuum = React.useCallback(async () => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.vacuum();
|
return await SQLiteDatabase.vacuum();
|
||||||
} catch (vacuumError) {
|
} catch (_vacuumError) {
|
||||||
console.error('Ошибка оптимизации базы данных:', vacuumError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [isDbReady]);
|
}, [isDbReady]);
|
||||||
@ -235,15 +141,11 @@ function useLocalDb() {
|
|||||||
//Проверка существования таблицы
|
//Проверка существования таблицы
|
||||||
const checkTableExists = React.useCallback(
|
const checkTableExists = React.useCallback(
|
||||||
async tableName => {
|
async tableName => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.checkTableExists(tableName);
|
return await SQLiteDatabase.checkTableExists(tableName);
|
||||||
} catch (checkTableError) {
|
} catch (_checkTableError) {
|
||||||
console.error('Ошибка проверки существования таблицы:', checkTableError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -253,15 +155,11 @@ function useLocalDb() {
|
|||||||
//Сохранение сессии авторизации
|
//Сохранение сессии авторизации
|
||||||
const setAuthSession = React.useCallback(
|
const setAuthSession = React.useCallback(
|
||||||
async session => {
|
async session => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.setAuthSession(session);
|
return await SQLiteDatabase.setAuthSession(session);
|
||||||
} catch (setAuthSessionError) {
|
} catch (_setAuthSessionError) {
|
||||||
console.error('Ошибка сохранения сессии авторизации:', setAuthSessionError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -270,46 +168,34 @@ function useLocalDb() {
|
|||||||
|
|
||||||
//Получение сессии авторизации
|
//Получение сессии авторизации
|
||||||
const getAuthSession = React.useCallback(async () => {
|
const getAuthSession = React.useCallback(async () => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return null;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.getAuthSession();
|
return await SQLiteDatabase.getAuthSession();
|
||||||
} catch (getAuthSessionError) {
|
} catch (_getAuthSessionError) {
|
||||||
console.error('Ошибка получения сессии авторизации:', getAuthSessionError);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [isDbReady]);
|
}, [isDbReady]);
|
||||||
|
|
||||||
//Очистка сессии авторизации
|
//Очистка сессии авторизации
|
||||||
const clearAuthSession = React.useCallback(async () => {
|
const clearAuthSession = React.useCallback(async () => {
|
||||||
if (!isDbReady) {
|
if (!isDbReady) return false;
|
||||||
console.warn('База данных не готова');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await SQLiteDatabase.clearAuthSession();
|
return await SQLiteDatabase.clearAuthSession();
|
||||||
} catch (clearAuthSessionError) {
|
} catch (_clearAuthSessionError) {
|
||||||
console.error('Ошибка очистки сессии авторизации:', clearAuthSessionError);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [isDbReady]);
|
}, [isDbReady]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDbReady,
|
isDbReady,
|
||||||
inspections,
|
|
||||||
error: dbError,
|
error: dbError,
|
||||||
loadInspections,
|
|
||||||
saveInspection,
|
|
||||||
getSetting,
|
getSetting,
|
||||||
setSetting,
|
setSetting,
|
||||||
deleteSetting,
|
deleteSetting,
|
||||||
getAllSettings,
|
getAllSettings,
|
||||||
clearSettings,
|
clearSettings,
|
||||||
clearInspections,
|
|
||||||
vacuum,
|
vacuum,
|
||||||
checkTableExists,
|
checkTableExists,
|
||||||
setAuthSession,
|
setAuthSession,
|
||||||
|
|||||||
76
rn/app/src/hooks/useScanInputHandler.js
Normal file
76
rn/app/src/hooks/useScanInputHandler.js
Normal 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;
|
||||||
@ -23,12 +23,17 @@ const OrganizationSelectDialog = require('../components/auth/OrganizationSelectD
|
|||||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
||||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||||
|
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||||
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
||||||
const { isServerUrlFieldVisible } = require('../utils/loginFormUtils'); //Утилиты формы входа
|
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 { normalizeServerUrl, validateServerUrl } = require('../utils/validation'); //Валидация
|
||||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Конфиг авторизации
|
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 { getAuthFormStore, setAuthFormStore, clearAuthFormStore } = require('../utils/authFormStore'); //Хранилище формы входа
|
||||||
const {
|
const {
|
||||||
APP_ABOUT_TITLE,
|
APP_ABOUT_TITLE,
|
||||||
@ -36,14 +41,22 @@ const {
|
|||||||
ORGANIZATION_SELECT_DIALOG_TITLE,
|
ORGANIZATION_SELECT_DIALOG_TITLE,
|
||||||
MENU_ITEM_SETTINGS,
|
MENU_ITEM_SETTINGS,
|
||||||
MENU_ITEM_ABOUT,
|
MENU_ITEM_ABOUT,
|
||||||
|
MENU_ITEM_LOGOUT,
|
||||||
AUTH_SCREEN_TITLE,
|
AUTH_SCREEN_TITLE,
|
||||||
AUTH_BUTTON_LOGIN,
|
AUTH_BUTTON_LOGIN,
|
||||||
AUTH_BUTTON_LOADING,
|
AUTH_BUTTON_LOADING,
|
||||||
|
AUTH_BUTTON_LOGOUT,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
AUTH_SERVER_CHANGE_CONFIRM_TITLE,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
AUTH_SERVER_CHANGE_CONFIRM_MESSAGE,
|
||||||
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||||
AUTH_SERVER_CHANGE_CANCEL_BUTTON
|
AUTH_SERVER_CHANGE_CANCEL_BUTTON,
|
||||||
|
LOGOUT_CONFIRM_TITLE,
|
||||||
|
LOGOUT_CONFIRM_MESSAGE
|
||||||
} = require('../config/messages'); //Сообщения
|
} = 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'); //Стили экрана
|
const styles = require('../styles/screens/AuthScreen.styles'); //Стили экрана
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
@ -53,12 +66,13 @@ const styles = require('../styles/screens/AuthScreen.styles'); //Стили эк
|
|||||||
//Экран аутентификации
|
//Экран аутентификации
|
||||||
function AuthScreen() {
|
function AuthScreen() {
|
||||||
const { showError, showInfo } = useAppMessagingContext();
|
const { showError, showInfo } = useAppMessagingContext();
|
||||||
const { APP_MODE, mode, setOnline } = useAppModeContext();
|
const { APP_MODE, mode, setOnline, setNotConnected } = useAppModeContext();
|
||||||
const { navigate, goBack, canGoBack, reset, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
|
const { navigate, goBack, canGoBack, reset, setInitialScreen, screenParams, currentScreen, SCREENS } = useAppNavigationContext();
|
||||||
const { getSetting, isDbReady, clearInspections } = useAppLocalDbContext();
|
const { getSetting, isDbReady } = useAppLocalDbContext();
|
||||||
const {
|
const {
|
||||||
session,
|
session,
|
||||||
login,
|
login,
|
||||||
|
logout,
|
||||||
selectCompany,
|
selectCompany,
|
||||||
isLoading,
|
isLoading,
|
||||||
getSavedCredentials,
|
getSavedCredentials,
|
||||||
@ -115,6 +129,42 @@ function AuthScreen() {
|
|||||||
const loginInputRef = React.useRef(null);
|
const loginInputRef = React.useRef(null);
|
||||||
const passwordInputRef = 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
|
//Актуализация ref и store при изменении полей; при размонтировании — сохранение в store
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const next = { serverUrl, username, password, savePassword, showPassword };
|
const next = { serverUrl, username, password, savePassword, showPassword };
|
||||||
@ -183,7 +233,7 @@ function AuthScreen() {
|
|||||||
setUsername(val.trim());
|
setUsername(val.trim());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(noopCatch);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
@ -240,7 +290,6 @@ function AuthScreen() {
|
|||||||
|
|
||||||
initialLoadRef.current = true;
|
initialLoadRef.current = true;
|
||||||
} catch (loadError) {
|
} catch (loadError) {
|
||||||
console.error('Ошибка загрузки credentials из меню:', loadError);
|
|
||||||
setUsername('');
|
setUsername('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setSavePassword(false);
|
setSavePassword(false);
|
||||||
@ -264,9 +313,7 @@ function AuthScreen() {
|
|||||||
if (savedLogin && typeof savedLogin === 'string') {
|
if (savedLogin && typeof savedLogin === 'string') {
|
||||||
setUsername(savedLogin.trim());
|
setUsername(savedLogin.trim());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_e) {}
|
||||||
console.warn('Резервная загрузка логина:', e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSavedLoginOnly();
|
loadSavedLoginOnly();
|
||||||
@ -288,9 +335,7 @@ function AuthScreen() {
|
|||||||
}
|
}
|
||||||
setSavedServerUrlFromSettings(trimmedUrl);
|
setSavedServerUrlFromSettings(trimmedUrl);
|
||||||
setHideServerUrl(savedHide === 'true' || savedHide === true);
|
setHideServerUrl(savedHide === 'true' || savedHide === true);
|
||||||
} catch (e) {
|
} catch (_e) {}
|
||||||
console.warn('Загрузка адреса сервера и настройки видимости:', e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadServerAndVisibility();
|
loadServerAndVisibility();
|
||||||
@ -338,8 +383,7 @@ function AuthScreen() {
|
|||||||
setPassword('');
|
setPassword('');
|
||||||
setSavePassword(false);
|
setSavePassword(false);
|
||||||
}
|
}
|
||||||
} catch (loadError) {
|
} catch (_loadError) {
|
||||||
console.error('Ошибка загрузки настроек авторизации:', loadError);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSettingsLoaded(true);
|
setIsSettingsLoaded(true);
|
||||||
}
|
}
|
||||||
@ -372,20 +416,25 @@ function AuthScreen() {
|
|||||||
}, [hideServerUrl, serverUrl, username, password, showError]);
|
}, [hideServerUrl, serverUrl, username, password, showError]);
|
||||||
|
|
||||||
//Выполнение входа
|
//Выполнение входа
|
||||||
const performLogin = React.useCallback(async () => {
|
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);
|
||||||
|
|
||||||
let idleTimeout = null;
|
let idleTimeout = null;
|
||||||
try {
|
try {
|
||||||
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||||
} catch (settingError) {
|
} catch (_settingError) {}
|
||||||
console.warn('Ошибка получения таймаута:', settingError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await login({
|
const result = await login({
|
||||||
serverUrl: serverUrl.trim(),
|
serverUrl: s,
|
||||||
user: username.trim(),
|
user: u,
|
||||||
password: password.trim(),
|
password: p,
|
||||||
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
|
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
|
||||||
savePassword
|
savePassword: save
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@ -410,7 +459,34 @@ function AuthScreen() {
|
|||||||
clearAuthFormData();
|
clearAuthFormData();
|
||||||
clearAuthFormStore();
|
clearAuthFormStore();
|
||||||
reset();
|
reset();
|
||||||
}, [login, serverUrl, username, password, savePassword, getSetting, showError, setOnline, clearAuthFormData, 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;
|
||||||
|
}
|
||||||
|
if (!p) {
|
||||||
|
showError('Укажите пароль');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
performLogin(form);
|
||||||
|
}, [hideServerUrl, showError, performLogin]);
|
||||||
|
|
||||||
//Обработчик входа (очистка локальных данных только при смене сервера относительно последнего успешного подключения)
|
//Обработчик входа (очистка локальных данных только при смене сервера относительно последнего успешного подключения)
|
||||||
const handleLogin = React.useCallback(async () => {
|
const handleLogin = React.useCallback(async () => {
|
||||||
@ -440,7 +516,6 @@ function AuthScreen() {
|
|||||||
title: AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
title: AUTH_SERVER_CHANGE_CONFIRM_BUTTON,
|
||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
await clearAuthSession();
|
await clearAuthSession();
|
||||||
await clearInspections();
|
|
||||||
await performLogin();
|
await performLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -449,7 +524,7 @@ function AuthScreen() {
|
|||||||
} else {
|
} else {
|
||||||
await performLogin();
|
await performLogin();
|
||||||
}
|
}
|
||||||
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, clearInspections, performLogin]);
|
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, performLogin]);
|
||||||
|
|
||||||
//Обработчик выбора организации
|
//Обработчик выбора организации
|
||||||
const handleSelectOrganization = React.useCallback(
|
const handleSelectOrganization = React.useCallback(
|
||||||
@ -536,6 +611,60 @@ function AuthScreen() {
|
|||||||
handleLogin();
|
handleLogin();
|
||||||
}, [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(() => {
|
const handleMenuOpen = React.useCallback(() => {
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
@ -564,24 +693,67 @@ function AuthScreen() {
|
|||||||
});
|
});
|
||||||
}, [showInfo, mode, serverUrl, isDbReady]);
|
}, [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(() => {
|
const menuItems = React.useMemo(() => {
|
||||||
return [
|
const items = [
|
||||||
{
|
{
|
||||||
id: 'settings',
|
id: MENU_ITEM_ID_SETTINGS,
|
||||||
title: MENU_ITEM_SETTINGS,
|
title: MENU_ITEM_SETTINGS,
|
||||||
onPress: handleOpenSettings
|
onPress: handleOpenSettings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'about',
|
id: MENU_ITEM_ID_ABOUT,
|
||||||
title: MENU_ITEM_ABOUT,
|
title: MENU_ITEM_ABOUT,
|
||||||
onPress: handleShowAbout
|
onPress: handleShowAbout
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, [handleOpenSettings, handleShowAbout]);
|
|
||||||
|
|
||||||
//Поле сервера показываем, если настройка выключена или в настройках ещё не сохранён адрес
|
if (isFromMenu) {
|
||||||
const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, savedServerUrlFromSettings);
|
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 (
|
return (
|
||||||
<AdaptiveView padding={false}>
|
<AdaptiveView padding={false}>
|
||||||
@ -622,6 +794,8 @@ function AuthScreen() {
|
|||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
onSubmitEditing={handleServerSubmitEditing}
|
onSubmitEditing={handleServerSubmitEditing}
|
||||||
|
onFocus={handleServerFocus}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -638,6 +812,8 @@ function AuthScreen() {
|
|||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
onSubmitEditing={handleLoginSubmitEditing}
|
onSubmitEditing={handleLoginSubmitEditing}
|
||||||
|
onFocus={handleLoginFocus}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
@ -653,6 +829,8 @@ function AuthScreen() {
|
|||||||
blurOnSubmit={true}
|
blurOnSubmit={true}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onSubmitEditing={handlePasswordSubmitEditing}
|
onSubmitEditing={handlePasswordSubmitEditing}
|
||||||
|
onFocus={handlePasswordFocus}
|
||||||
|
onBlur={handleFieldBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.switchContainer}>
|
<View style={styles.switchContainer}>
|
||||||
@ -666,6 +844,16 @@ function AuthScreen() {
|
|||||||
style={styles.loginButton}
|
style={styles.loginButton}
|
||||||
textStyle={styles.loginButtonText}
|
textStyle={styles.loginButtonText}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isFromMenu ? (
|
||||||
|
<AppButton
|
||||||
|
title={AUTH_BUTTON_LOGOUT}
|
||||||
|
onPress={handleLogoutFromAuth}
|
||||||
|
disabled={isLoading}
|
||||||
|
style={styles.logoutButton}
|
||||||
|
textStyle={styles.logoutButtonText}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const { View } = require('react-native'); //Базовые компоненты
|
|||||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
||||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||||
|
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||||
const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
|
const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
|
||||||
@ -20,10 +21,25 @@ const ScannerArea = require('../components/scanner/ScannerArea'); //Област
|
|||||||
const ScanResultModal = require('../components/scanner/ScanResultModal'); //Модальное окно результата сканирования
|
const ScanResultModal = require('../components/scanner/ScanResultModal'); //Модальное окно результата сканирования
|
||||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
|
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig'); //Ключи настроек
|
||||||
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
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 { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||||
|
const { createLogoutHandler } = require('../utils/logoutFlow'); //Универсальный поток выхода
|
||||||
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
|
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'); //Стили экрана
|
const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
@ -32,7 +48,7 @@ const styles = require('../styles/screens/MainScreen.styles'); //Стили эк
|
|||||||
|
|
||||||
//Главный экран приложения
|
//Главный экран приложения
|
||||||
function MainScreen() {
|
function MainScreen() {
|
||||||
const { showInfo, showError } = useAppMessagingContext();
|
const { showInfo, showError, state: messagingState } = useAppMessagingContext();
|
||||||
const { mode, setNotConnected } = useAppModeContext();
|
const { mode, setNotConnected } = useAppModeContext();
|
||||||
const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext();
|
const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext();
|
||||||
const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
|
const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
|
||||||
@ -41,7 +57,60 @@ function MainScreen() {
|
|||||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||||
const [serverUrl, setServerUrl] = React.useState('');
|
const [serverUrl, setServerUrl] = React.useState('');
|
||||||
const [alwaysShowScanner, setAlwaysShowScanner] = React.useState(false);
|
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 [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(() => {
|
React.useEffect(() => {
|
||||||
@ -58,9 +127,10 @@ function MainScreen() {
|
|||||||
|
|
||||||
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
||||||
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
|
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();
|
loadMainScreenSettings();
|
||||||
@ -99,38 +169,37 @@ function MainScreen() {
|
|||||||
navigate(SCREENS.SETTINGS);
|
navigate(SCREENS.SETTINGS);
|
||||||
}, [navigate, SCREENS.SETTINGS]);
|
}, [navigate, SCREENS.SETTINGS]);
|
||||||
|
|
||||||
//Выполнение выхода (для диалога подтверждения)
|
//Универсальный обработчик выхода
|
||||||
const performLogout = React.useCallback(async () => {
|
const { handleLogout } = React.useMemo(
|
||||||
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
|
() =>
|
||||||
|
createLogoutHandler({
|
||||||
if (result.success) {
|
logout,
|
||||||
setNotConnected();
|
mode,
|
||||||
setInitialScreen(SCREENS.AUTH);
|
setNotConnected,
|
||||||
} else {
|
setInitialScreen,
|
||||||
showError(result.error || 'Ошибка выхода');
|
SCREENS,
|
||||||
}
|
showError,
|
||||||
}, [logout, mode, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]);
|
showInfo,
|
||||||
|
logoutConfirmTitle: LOGOUT_CONFIRM_TITLE,
|
||||||
//Обработчик выхода из приложения
|
logoutConfirmMessage: LOGOUT_CONFIRM_MESSAGE,
|
||||||
const handleLogout = React.useCallback(() => {
|
logoutButtonTitle: AUTH_BUTTON_LOGOUT,
|
||||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Выйти', performLogout);
|
getConfirmButtonOptions,
|
||||||
|
dialogButtonTypeError: DIALOG_BUTTON_TYPE.ERROR,
|
||||||
showInfo('Вы уверены, что хотите выйти?', {
|
dialogCancelButton: DIALOG_CANCEL_BUTTON
|
||||||
title: 'Подтверждение выхода',
|
}),
|
||||||
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
|
[logout, mode, setNotConnected, setInitialScreen, SCREENS, showError, showInfo]
|
||||||
});
|
);
|
||||||
}, [showInfo, performLogout]);
|
|
||||||
|
|
||||||
//Пункты бокового меню
|
//Пункты бокового меню
|
||||||
const menuItems = React.useMemo(() => {
|
const menuItems = React.useMemo(() => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
id: 'settings',
|
id: MENU_ITEM_ID_SETTINGS,
|
||||||
title: MENU_ITEM_SETTINGS,
|
title: MENU_ITEM_SETTINGS,
|
||||||
onPress: handleOpenSettings
|
onPress: handleOpenSettings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'about',
|
id: MENU_ITEM_ID_ABOUT,
|
||||||
title: MENU_ITEM_ABOUT,
|
title: MENU_ITEM_ABOUT,
|
||||||
onPress: handleShowAbout
|
onPress: handleShowAbout
|
||||||
}
|
}
|
||||||
@ -138,14 +207,14 @@ function MainScreen() {
|
|||||||
|
|
||||||
//Добавляем разделитель перед кнопками авторизации
|
//Добавляем разделитель перед кнопками авторизации
|
||||||
items.push({
|
items.push({
|
||||||
id: 'divider',
|
id: MENU_ITEM_ID_DIVIDER,
|
||||||
type: 'divider'
|
type: 'divider'
|
||||||
});
|
});
|
||||||
|
|
||||||
//Кнопка "Вход" для оффлайн режима
|
//Кнопка "Вход" для оффлайн режима
|
||||||
if (mode === 'OFFLINE') {
|
if (mode === 'OFFLINE') {
|
||||||
items.push({
|
items.push({
|
||||||
id: 'login',
|
id: MENU_ITEM_ID_LOGIN,
|
||||||
title: MENU_ITEM_LOGIN,
|
title: MENU_ITEM_LOGIN,
|
||||||
onPress: handleLogin
|
onPress: handleLogin
|
||||||
});
|
});
|
||||||
@ -154,10 +223,11 @@ function MainScreen() {
|
|||||||
//Кнопка "Выход" для онлайн/оффлайн режима (когда есть сессия)
|
//Кнопка "Выход" для онлайн/оффлайн режима (когда есть сессия)
|
||||||
if ((mode === 'ONLINE' || mode === 'OFFLINE') && isAuthenticated) {
|
if ((mode === 'ONLINE' || mode === 'OFFLINE') && isAuthenticated) {
|
||||||
items.push({
|
items.push({
|
||||||
id: 'logout',
|
id: MENU_ITEM_ID_LOGOUT,
|
||||||
title: MENU_ITEM_LOGOUT,
|
title: MENU_ITEM_LOGOUT,
|
||||||
onPress: handleLogout,
|
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({
|
const isScannerOpen = shouldShowScanner({
|
||||||
alwaysShowScanner,
|
alwaysShowScanner,
|
||||||
hasScanResult: scanResult != null,
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<AppHeader onMenuPress={handleMenuOpen} />
|
<AppHeader onMenuPress={handleMenuOpen} />
|
||||||
|
|
||||||
<View style={styles.content}>
|
<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>
|
</View>
|
||||||
|
|
||||||
<SideMenu
|
<SideMenu
|
||||||
|
|||||||
@ -19,18 +19,22 @@ const AppHeader = require('../components/layout/AppHeader'); //Заголово
|
|||||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима
|
||||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||||
|
const { useHardwareScannerContext } = require('../components/layout/HardwareScannerProvider'); //Встроенный сканер
|
||||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
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 { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||||
const { getAppInfo, getModeLabel } = require('../utils/appInfo'); //Информация о приложении и режиме
|
const { getAppInfo, getModeLabel } = require('../utils/appInfo'); //Информация о приложении и режиме
|
||||||
const { validateServerUrlAllowEmpty, validateIdleTimeout } = require('../utils/validation'); //Валидация
|
const { validateServerUrlAllowEmpty, validateIdleTimeoutAllowEmpty, validateServerRequestTimeout } = require('../utils/validation'); //Валидация
|
||||||
const {
|
const {
|
||||||
APP_ABOUT_TITLE,
|
APP_ABOUT_TITLE,
|
||||||
SETTINGS_SERVER_SAVED_MESSAGE,
|
SETTINGS_SERVER_SAVED_MESSAGE,
|
||||||
SETTINGS_RESET_SUCCESS_MESSAGE,
|
SETTINGS_RESET_SUCCESS_MESSAGE,
|
||||||
MENU_ITEM_ABOUT,
|
MENU_ITEM_ABOUT,
|
||||||
SCANNER_SETTING_LABEL
|
SCANNER_SETTING_LABEL,
|
||||||
|
SCANNER_PRIORITY_LABEL,
|
||||||
|
SCANNER_PRIORITY_CAMERA,
|
||||||
|
SCANNER_PRIORITY_HARDWARE
|
||||||
} = require('../config/messages'); //Сообщения
|
} = require('../config/messages'); //Сообщения
|
||||||
const styles = require('../styles/screens/SettingsScreen.styles'); //Стили экрана
|
const styles = require('../styles/screens/SettingsScreen.styles'); //Стили экрана
|
||||||
|
|
||||||
@ -42,7 +46,7 @@ function SettingsScreen() {
|
|||||||
const { showInfo, showError, showSuccess } = useAppMessagingContext();
|
const { showInfo, showError, showSuccess } = useAppMessagingContext();
|
||||||
const { APP_MODE, mode, setNotConnected } = useAppModeContext();
|
const { APP_MODE, mode, setNotConnected } = useAppModeContext();
|
||||||
const { goBack, canGoBack } = useAppNavigationContext();
|
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 { session, isAuthenticated, getDeviceId, setLastSavedServerUrlFromSettings } = useAppAuthContext();
|
||||||
|
|
||||||
const [serverUrl, setServerUrl] = React.useState('');
|
const [serverUrl, setServerUrl] = React.useState('');
|
||||||
@ -52,11 +56,47 @@ function SettingsScreen() {
|
|||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
|
const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
|
||||||
const [isIdleTimeoutDialogVisible, setIsIdleTimeoutDialogVisible] = 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 [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 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(() => {
|
React.useEffect(() => {
|
||||||
//Выходим, если БД не готова или уже загрузили настройки
|
//Выходим, если БД не готова или уже загрузили настройки
|
||||||
@ -78,13 +118,16 @@ function SettingsScreen() {
|
|||||||
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
||||||
|
|
||||||
const savedIdleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
const savedIdleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||||
if (savedIdleTimeout) {
|
setIdleTimeout(savedIdleTimeout != null ? String(savedIdleTimeout).trim() : '');
|
||||||
setIdleTimeout(savedIdleTimeout);
|
|
||||||
|
const savedServerRequestTimeout = await getSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT);
|
||||||
|
const serverRequestTimeoutValue = savedServerRequestTimeout != null ? String(savedServerRequestTimeout).trim() : '';
|
||||||
|
if (serverRequestTimeoutValue) {
|
||||||
|
setServerRequestTimeout(serverRequestTimeoutValue);
|
||||||
} else {
|
} else {
|
||||||
//Устанавливаем значение по умолчанию
|
const defaultValue = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
setServerRequestTimeout(defaultValue);
|
||||||
setIdleTimeout(defaultValue);
|
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultValue);
|
||||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Получаем или генерируем идентификатор устройства
|
//Получаем или генерируем идентификатор устройства
|
||||||
@ -93,8 +136,10 @@ function SettingsScreen() {
|
|||||||
|
|
||||||
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
const savedAlwaysShowScanner = await getSetting(AUTH_SETTINGS_KEYS.ALWAYS_SHOW_SCANNER);
|
||||||
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
|
setAlwaysShowScanner(savedAlwaysShowScanner === 'true' || savedAlwaysShowScanner === true);
|
||||||
|
|
||||||
|
const savedPriority = await getSetting(AUTH_SETTINGS_KEYS.MAIN_SCANNER_PRIORITY);
|
||||||
|
setMainScannerPriority(savedPriority === 'hardware' ? 'hardware' : 'camera');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки настроек:', error);
|
|
||||||
showError('Не удалось загрузить настройки');
|
showError('Не удалось загрузить настройки');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -125,6 +170,12 @@ function SettingsScreen() {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Стиль поля времени ожидания сервера при нажатии
|
||||||
|
const getServerRequestTimeoutFieldPressableStyle = React.useCallback(
|
||||||
|
({ pressed }) => [styles.serverUrlField, pressed && styles.serverUrlFieldPressed],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
//Открытие диалога ввода URL сервера (только в режиме "Не подключено")
|
//Открытие диалога ввода URL сервера (только в режиме "Не подключено")
|
||||||
const handleOpenServerUrlDialog = React.useCallback(() => {
|
const handleOpenServerUrlDialog = React.useCallback(() => {
|
||||||
if (!isServerUrlEditable) {
|
if (!isServerUrlEditable) {
|
||||||
@ -159,7 +210,6 @@ function SettingsScreen() {
|
|||||||
showError('Не удалось сохранить настройки');
|
showError('Не удалось сохранить настройки');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения настроек:', error);
|
|
||||||
showError('Не удалось сохранить настройки');
|
showError('Не удалось сохранить настройки');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,13 +231,41 @@ function SettingsScreen() {
|
|||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения настройки сканера:', error);
|
|
||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSetting, showSuccess, 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 сервера в окне логина
|
//Переключатель скрытия URL сервера в окне логина
|
||||||
const handleToggleHideServerUrl = React.useCallback(
|
const handleToggleHideServerUrl = React.useCallback(
|
||||||
async value => {
|
async value => {
|
||||||
@ -201,7 +279,6 @@ function SettingsScreen() {
|
|||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения настройки:', error);
|
|
||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -218,6 +295,16 @@ function SettingsScreen() {
|
|||||||
setIsIdleTimeoutDialogVisible(false);
|
setIsIdleTimeoutDialogVisible(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
//Открытие диалога ввода времени ожидания ответа от сервера
|
||||||
|
const handleOpenServerRequestTimeoutDialog = React.useCallback(() => {
|
||||||
|
setIsServerRequestTimeoutDialogVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
//Закрытие диалога ввода времени ожидания ответа от сервера
|
||||||
|
const handleCloseServerRequestTimeoutDialog = React.useCallback(() => {
|
||||||
|
setIsServerRequestTimeoutDialogVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
//Сохранение времени простоя
|
//Сохранение времени простоя
|
||||||
const handleSaveIdleTimeout = React.useCallback(
|
const handleSaveIdleTimeout = React.useCallback(
|
||||||
async value => {
|
async value => {
|
||||||
@ -235,7 +322,6 @@ function SettingsScreen() {
|
|||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сохранения времени простоя:', error);
|
|
||||||
showError('Не удалось сохранить настройку');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,22 +330,37 @@ function SettingsScreen() {
|
|||||||
[setSetting, showSuccess, showError]
|
[setSetting, showSuccess, showError]
|
||||||
);
|
);
|
||||||
|
|
||||||
//Выполнение очистки кэша (для диалога подтверждения)
|
//Сохранение времени ожидания ответа от сервера
|
||||||
const performClearCache = React.useCallback(async () => {
|
const handleSaveServerRequestTimeout = React.useCallback(
|
||||||
|
async value => {
|
||||||
|
setIsServerRequestTimeoutDialogVisible(false);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await clearInspections();
|
const trimmedValue = value != null ? String(value).trim() : '';
|
||||||
|
const success = await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, trimmedValue);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
showSuccess('Кэш успешно очищен');
|
setServerRequestTimeout(trimmedValue);
|
||||||
|
showSuccess('Время ожидания сервера сохранено');
|
||||||
} else {
|
} else {
|
||||||
showError('Не удалось очистить кэш');
|
showError('Не удалось сохранить настройку');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка очистки кэша:', error);
|
showError('Не удалось сохранить настройку');
|
||||||
showError('Не удалось очистить кэш');
|
|
||||||
}
|
}
|
||||||
}, [showSuccess, showError, clearInspections]);
|
|
||||||
|
|
||||||
//Очистка кэша (осмотров)
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
[setSetting, showSuccess, showError]
|
||||||
|
);
|
||||||
|
|
||||||
|
//Выполнение очистки кэша
|
||||||
|
const performClearCache = React.useCallback(async () => {
|
||||||
|
showSuccess('Кэш успешно очищен');
|
||||||
|
}, [showSuccess]);
|
||||||
|
|
||||||
|
//Очистка кэша — диалог подтверждения, по подтверждению ничего не выполняется
|
||||||
const handleClearCache = React.useCallback(() => {
|
const handleClearCache = React.useCallback(() => {
|
||||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Очистить', performClearCache);
|
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Очистить', performClearCache);
|
||||||
|
|
||||||
@ -273,29 +374,32 @@ function SettingsScreen() {
|
|||||||
//Подключён (онлайн/офлайн): сбрасываем только непричастные к подключению настройки; не подключён: полный сброс
|
//Подключён (онлайн/офлайн): сбрасываем только непричастные к подключению настройки; не подключён: полный сброс
|
||||||
const performResetSettings = React.useCallback(async () => {
|
const performResetSettings = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
|
||||||
|
|
||||||
if (mode === APP_MODE.NOT_CONNECTED) {
|
if (mode === APP_MODE.NOT_CONNECTED) {
|
||||||
const success = await clearSettings();
|
const success = await clearSettings();
|
||||||
if (success) {
|
if (success) {
|
||||||
|
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||||
|
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
|
||||||
setServerUrl('');
|
setServerUrl('');
|
||||||
setHideServerUrl(false);
|
setHideServerUrl(false);
|
||||||
setAlwaysShowScanner(false);
|
setAlwaysShowScanner(false);
|
||||||
setIdleTimeout(defaultValue);
|
setMainScannerPriority('camera');
|
||||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
setIdleTimeout('');
|
||||||
|
setServerRequestTimeout(defaultServerTimeout);
|
||||||
setNotConnected();
|
setNotConnected();
|
||||||
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
||||||
} else {
|
} else {
|
||||||
showError('Не удалось сбросить настройки');
|
showError('Не удалось сбросить настройки');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//Подключён (онлайн или офлайн): сбрасываем только время простоя
|
//Подключён (онлайн или офлайн): сбрасываем только время простоя и таймаут сервера
|
||||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
const defaultServerTimeout = String(DEFAULT_SERVER_REQUEST_TIMEOUT);
|
||||||
setIdleTimeout(defaultValue);
|
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, '');
|
||||||
|
await setSetting(AUTH_SETTINGS_KEYS.SERVER_REQUEST_TIMEOUT, defaultServerTimeout);
|
||||||
|
setIdleTimeout('');
|
||||||
|
setServerRequestTimeout(defaultServerTimeout);
|
||||||
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
showSuccess(SETTINGS_RESET_SUCCESS_MESSAGE);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка сброса настроек:', error);
|
|
||||||
showError('Не удалось сбросить настройки');
|
showError('Не удалось сбросить настройки');
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@ -304,6 +408,7 @@ function SettingsScreen() {
|
|||||||
setServerUrl,
|
setServerUrl,
|
||||||
setHideServerUrl,
|
setHideServerUrl,
|
||||||
setIdleTimeout,
|
setIdleTimeout,
|
||||||
|
setServerRequestTimeout,
|
||||||
setNotConnected,
|
setNotConnected,
|
||||||
clearSettings,
|
clearSettings,
|
||||||
setSetting,
|
setSetting,
|
||||||
@ -332,7 +437,6 @@ function SettingsScreen() {
|
|||||||
showError('Не удалось оптимизировать базу данных');
|
showError('Не удалось оптимизировать базу данных');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка оптимизации БД:', error);
|
|
||||||
showError('Не удалось оптимизировать базу данных');
|
showError('Не удалось оптимизировать базу данных');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -443,6 +547,7 @@ function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{mode === APP_MODE.ONLINE || mode === APP_MODE.OFFLINE ? (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||||
Главный экран
|
Главный экран
|
||||||
@ -456,7 +561,32 @@ function SettingsScreen() {
|
|||||||
disabled={isLoading || !isDbReady}
|
disabled={isLoading || !isDbReady}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||||
@ -468,8 +598,22 @@ function SettingsScreen() {
|
|||||||
</AppText>
|
</AppText>
|
||||||
|
|
||||||
<Pressable style={getIdleTimeoutFieldPressableStyle} onPress={handleOpenIdleTimeoutDialog} disabled={isLoading || !isDbReady}>
|
<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}>
|
<AppText style={styles.serverUrlText} numberOfLines={1}>
|
||||||
{idleTimeout || String(DEFAULT_IDLE_TIMEOUT)}
|
{serverRequestTimeout || String(DEFAULT_SERVER_REQUEST_TIMEOUT)}
|
||||||
</AppText>
|
</AppText>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
@ -562,6 +706,7 @@ function SettingsScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<InputDialog
|
<InputDialog
|
||||||
|
ref={serverUrlDialogRef}
|
||||||
visible={isServerUrlDialogVisible}
|
visible={isServerUrlDialogVisible}
|
||||||
title="Адрес сервера"
|
title="Адрес сервера"
|
||||||
label="URL сервера приложений"
|
label="URL сервера приложений"
|
||||||
@ -576,17 +721,33 @@ function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<InputDialog
|
<InputDialog
|
||||||
|
ref={idleTimeoutDialogRef}
|
||||||
visible={isIdleTimeoutDialogVisible}
|
visible={isIdleTimeoutDialogVisible}
|
||||||
title="Время простоя"
|
title="Время простоя"
|
||||||
label="Максимальное время простоя (минут)"
|
label="Максимальное время простоя (минут)"
|
||||||
value={idleTimeout}
|
value={idleTimeout}
|
||||||
placeholder="Например: 30"
|
placeholder="Например: 30 или пусто"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
confirmText="Сохранить"
|
confirmText="Сохранить"
|
||||||
cancelText="Отмена"
|
cancelText="Отмена"
|
||||||
onConfirm={handleSaveIdleTimeout}
|
onConfirm={handleSaveIdleTimeout}
|
||||||
onCancel={handleCloseIdleTimeoutDialog}
|
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>
|
</AdaptiveView>
|
||||||
);
|
);
|
||||||
|
|||||||
78
rn/app/src/services/HardwareScannerBridge.js
Normal file
78
rn/app/src/services/HardwareScannerBridge.js
Normal 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
|
||||||
|
};
|
||||||
@ -83,14 +83,17 @@ const styles = StyleSheet.create({
|
|||||||
color: APP_COLORS.textSecondary
|
color: APP_COLORS.textSecondary
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
height: UI.INPUT_HEIGHT,
|
minHeight: UI.INPUT_HEIGHT,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: APP_COLORS.borderMedium,
|
borderColor: APP_COLORS.borderMedium,
|
||||||
borderRadius: UI.BORDER_RADIUS,
|
borderRadius: UI.BORDER_RADIUS,
|
||||||
paddingHorizontal: responsiveSpacing(3),
|
paddingHorizontal: responsiveSpacing(3),
|
||||||
|
paddingVertical: responsiveSpacing(2.5),
|
||||||
fontSize: UI.FONT_SIZE_MD,
|
fontSize: UI.FONT_SIZE_MD,
|
||||||
color: APP_COLORS.textPrimary,
|
color: APP_COLORS.textPrimary,
|
||||||
backgroundColor: APP_COLORS.surface
|
backgroundColor: APP_COLORS.surface,
|
||||||
|
includeFontPadding: false,
|
||||||
|
textAlignVertical: 'center'
|
||||||
},
|
},
|
||||||
inputFocused: {
|
inputFocused: {
|
||||||
borderColor: APP_COLORS.primary,
|
borderColor: APP_COLORS.primary,
|
||||||
|
|||||||
26
rn/app/src/styles/layout/AppIdleProvider.styles.js
Normal file
26
rn/app/src/styles/layout/AppIdleProvider.styles.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
Предрейсовые осмотры - мобильное приложение
|
||||||
|
Стили обёртки провайдера простоя
|
||||||
|
*/
|
||||||
|
|
||||||
|
//---------------------
|
||||||
|
//Подключение библиотек
|
||||||
|
//---------------------
|
||||||
|
|
||||||
|
const { StyleSheet } = require('react-native'); //StyleSheet
|
||||||
|
|
||||||
|
//-----------
|
||||||
|
//Тело модуля
|
||||||
|
//-----------
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
touchArea: {
|
||||||
|
flex: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//----------------
|
||||||
|
//Интерфейс модуля
|
||||||
|
//----------------
|
||||||
|
|
||||||
|
module.exports = styles;
|
||||||
112
rn/app/src/styles/menu/MenuItemIcon.styles.js
Normal file
112
rn/app/src/styles/menu/MenuItemIcon.styles.js
Normal 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;
|
||||||
48
rn/app/src/styles/scanner/CameraPausedOverlay.styles.js
Normal file
48
rn/app/src/styles/scanner/CameraPausedOverlay.styles.js
Normal 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;
|
||||||
@ -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 { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||||
const { UI } = require('../../config/appConfig'); //Конфигурация UI
|
const { UI } = require('../../config/appConfig'); //Конфигурация UI
|
||||||
const { responsiveSpacing, widthPercentage } = require('../../utils/responsive'); //Адаптивные утилиты
|
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({
|
const styles = StyleSheet.create({
|
||||||
backdrop: {
|
modalRoot: {
|
||||||
flex: 1,
|
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,
|
backgroundColor: APP_COLORS.overlay,
|
||||||
|
zIndex: 1000
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: UI.PADDING
|
paddingHorizontal: UI.PADDING
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
width: '100%',
|
width: resultCardWidth,
|
||||||
maxWidth: widthPercentage(90),
|
minWidth: resultCardWidth,
|
||||||
|
maxWidth: resultCardWidth,
|
||||||
backgroundColor: APP_COLORS.surface,
|
backgroundColor: APP_COLORS.surface,
|
||||||
borderRadius: UI.BORDER_RADIUS,
|
borderRadius: UI.BORDER_RADIUS,
|
||||||
...Platform.select({
|
...Platform.select({
|
||||||
@ -83,6 +115,7 @@ const styles = StyleSheet.create({
|
|||||||
color: APP_COLORS.textSecondary
|
color: APP_COLORS.textSecondary
|
||||||
},
|
},
|
||||||
valueBlock: {
|
valueBlock: {
|
||||||
|
width: '100%',
|
||||||
paddingVertical: responsiveSpacing(2),
|
paddingVertical: responsiveSpacing(2),
|
||||||
paddingHorizontal: responsiveSpacing(3),
|
paddingHorizontal: responsiveSpacing(3),
|
||||||
backgroundColor: APP_COLORS.surfaceAlt,
|
backgroundColor: APP_COLORS.surfaceAlt,
|
||||||
@ -90,6 +123,7 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: responsiveSpacing(4)
|
marginBottom: responsiveSpacing(4)
|
||||||
},
|
},
|
||||||
valueText: {
|
valueText: {
|
||||||
|
width: '100%',
|
||||||
fontSize: UI.FONT_SIZE_MD,
|
fontSize: UI.FONT_SIZE_MD,
|
||||||
color: APP_COLORS.textPrimary
|
color: APP_COLORS.textPrimary
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,6 +23,14 @@ const styles = StyleSheet.create({
|
|||||||
minHeight: responsiveSpacing(30),
|
minHeight: responsiveSpacing(30),
|
||||||
marginBottom: responsiveSpacing(2)
|
marginBottom: responsiveSpacing(2)
|
||||||
},
|
},
|
||||||
|
scannerWrapper: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%'
|
||||||
|
},
|
||||||
|
//Область тапа для включения камеры (оверлей поверх сканера на паузе)
|
||||||
|
cameraPausedOverlayTouchable: {
|
||||||
|
...StyleSheet.absoluteFillObject
|
||||||
|
},
|
||||||
modalContainer: {
|
modalContainer: {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,6 +28,15 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
minWidth: responsiveSpacing(40)
|
minWidth: responsiveSpacing(40)
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
textAlign: 'center',
|
||||||
|
color: APP_COLORS.textSecondary,
|
||||||
|
marginBottom: responsiveSpacing(3)
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
marginTop: responsiveSpacing(2),
|
||||||
|
minWidth: responsiveSpacing(40)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,16 @@ const styles = StyleSheet.create({
|
|||||||
color: APP_COLORS.white,
|
color: APP_COLORS.white,
|
||||||
fontWeight: '600'
|
fontWeight: '600'
|
||||||
},
|
},
|
||||||
|
logoutButton: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: APP_COLORS.error,
|
||||||
|
marginTop: responsiveSpacing(3)
|
||||||
|
},
|
||||||
|
logoutButtonText: {
|
||||||
|
color: APP_COLORS.error,
|
||||||
|
fontWeight: '600'
|
||||||
|
},
|
||||||
hint: {
|
hint: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginTop: responsiveSpacing(4),
|
marginTop: responsiveSpacing(4),
|
||||||
|
|||||||
@ -88,6 +88,33 @@ const styles = StyleSheet.create({
|
|||||||
switchRow: {
|
switchRow: {
|
||||||
marginTop: responsiveSpacing(3)
|
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: {
|
actionButton: {
|
||||||
marginTop: responsiveSpacing(3)
|
marginTop: responsiveSpacing(3)
|
||||||
},
|
},
|
||||||
|
|||||||
53
rn/app/src/utils/authFormFieldsOrder.js
Normal file
53
rn/app/src/utils/authFormFieldsOrder.js
Normal 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
|
||||||
|
};
|
||||||
31
rn/app/src/utils/authScannerUtils.js
Normal file
31
rn/app/src/utils/authScannerUtils.js
Normal 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
|
||||||
|
};
|
||||||
@ -107,8 +107,7 @@ const getAndroidId = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.warn('Не удалось получить Android ID:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -134,8 +133,7 @@ const getIosId = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
console.warn('Не удалось получить iOS ID:', error);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
61
rn/app/src/utils/logoutFlow.js
Normal file
61
rn/app/src/utils/logoutFlow.js
Normal 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
23
rn/app/src/utils/noop.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Предрейсовые осмотры - мобильное приложение
|
||||||
|
Пустые функции для использования в обработчиках и .catch()
|
||||||
|
*/
|
||||||
|
|
||||||
|
//---------
|
||||||
|
//Функции
|
||||||
|
//---------
|
||||||
|
|
||||||
|
//Пустая функция (например для onPress, чтобы не передавать клик дальше)
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
//Пустой обработчик для .catch() — подавляет ошибку без побочных эффектов
|
||||||
|
function noopCatch() {}
|
||||||
|
|
||||||
|
//----------------
|
||||||
|
//Интерфейс модуля
|
||||||
|
//----------------
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
noop,
|
||||||
|
noopCatch
|
||||||
|
};
|
||||||
39
rn/app/src/utils/scanInputTargetResolver.js
Normal file
39
rn/app/src/utils/scanInputTargetResolver.js
Normal 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
|
||||||
|
};
|
||||||
@ -112,7 +112,6 @@ const encryptData = (data, secretKey, salt = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
console.error('Ошибка шифрования: секретный ключ не предоставлен');
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +120,6 @@ const encryptData = (data, secretKey, salt = '') => {
|
|||||||
const encrypted = xorEncrypt(data, keyHash);
|
const encrypted = xorEncrypt(data, keyHash);
|
||||||
return ENCRYPTED_PREFIX + encrypted;
|
return ENCRYPTED_PREFIX + encrypted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка шифрования:', error);
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -133,7 +131,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!secretKey) {
|
if (!secretKey) {
|
||||||
console.error('Ошибка расшифровки: секретный ключ не предоставлен');
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +145,6 @@ const decryptData = (encryptedData, secretKey, salt = '') => {
|
|||||||
const keyHash = generateKeyHash(secretKey, salt);
|
const keyHash = generateKeyHash(secretKey, salt);
|
||||||
return xorDecrypt(encryptedHex, keyHash);
|
return xorDecrypt(encryptedHex, keyHash);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка расшифровки:', error);
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
40
rn/app/src/utils/serverRequestTimeout.js
Normal file
40
rn/app/src/utils/serverRequestTimeout.js
Normal 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
|
||||||
|
};
|
||||||
@ -19,7 +19,10 @@ const VALIDATION_MESSAGES = {
|
|||||||
SERVER_URL_INVALID_SHORT: 'Некорректный формат URL',
|
SERVER_URL_INVALID_SHORT: 'Некорректный формат URL',
|
||||||
IDLE_TIMEOUT_EMPTY: 'Введите время простоя',
|
IDLE_TIMEOUT_EMPTY: 'Введите время простоя',
|
||||||
IDLE_TIMEOUT_MIN: 'Введите положительное число (минимум 1 минута)',
|
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;
|
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,
|
normalizeServerUrl,
|
||||||
validateServerUrl,
|
validateServerUrl,
|
||||||
validateServerUrlAllowEmpty,
|
validateServerUrlAllowEmpty,
|
||||||
validateIdleTimeout
|
validateIdleTimeout,
|
||||||
|
validateIdleTimeoutAllowEmpty,
|
||||||
|
validateServerRequestTimeout
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user