diff --git a/rn/app/.eslintrc.js b/rn/app/.eslintrc.js
new file mode 100644
index 0000000..38acfe8
--- /dev/null
+++ b/rn/app/.eslintrc.js
@@ -0,0 +1,36 @@
+const path = require('path');
+
+module.exports = {
+ root: true,
+ extends: '@react-native',
+ parserOptions: {
+ ecmaVersion: 2021,
+ sourceType: 'module',
+ requireConfigFile: false,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ babelOptions: {
+ configFile: path.resolve(__dirname, 'babel.config.js'),
+ },
+ },
+ env: {
+ 'react-native/react-native': true,
+ es2021: true,
+ node: true,
+ },
+ overrides: [
+ {
+ files: ['*.js', '*.jsx'],
+ parserOptions: {
+ requireConfigFile: false,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ babelOptions: {
+ configFile: path.resolve(__dirname, 'babel.config.js'),
+ },
+ },
+ },
+ ],
+};
diff --git a/rn/app/.gitignore b/rn/app/.gitignore
new file mode 100644
index 0000000..43ad15a
--- /dev/null
+++ b/rn/app/.gitignore
@@ -0,0 +1,787 @@
+# -------------------------------------------------------------------
+# OPERATING SYSTEM FILES
+# -------------------------------------------------------------------
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+.Spotlight-V100
+.Trashes
+._*
+.fseventsd
+
+# Windows
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+$RECYCLE.BIN/
+*.cab
+*.msi
+*.msm
+*.msp
+*.lnk
+
+# Linux
+*~
+.directory
+*.swp
+*.swo
+*.pid
+.session
+.viminfo
+
+# Cross-platform
+.nfs*
+*.lock
+*.pid
+*.seed
+*.pid.lock
+
+# -------------------------------------------------------------------
+# IDE AND EDITOR CONFIGURATIONS
+# -------------------------------------------------------------------
+# VS Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.vscode/*.code-workspace
+
+# IntelliJ IDEA
+.idea/
+*.iml
+*.iws
+*.ipr
+modules.xml
+workspace.xml
+
+# Eclipse
+.project
+.classpath
+.settings/
+.cache/
+.metadata/
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+
+# Sublime Text
+*.sublime-workspace
+*.sublime-project
+*.sublime-gps
+*.sublime-gps-cache
+
+# Atom
+.atom/
+.atomic/
+.blobs/
+.compile-cache/
+.node-gyp/
+.npm/
+.snap/
+.storage/
+
+# Emacs
+*~
+\#*\#
+.\#*
+auto-save-list
+tramp
+.#*
+
+# Vim
+*.swp
+*.swo
+*.un~
+Session.vim
+.netrwhist
+
+# -------------------------------------------------------------------
+# NODE.JS AND JAVASCRIPT
+# -------------------------------------------------------------------
+# Dependencies
+node_modules/
+.node_modules/
+npm-packages-offline-cache/
+
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+pnpm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage/
+.nyc_output/
+
+# Grunt intermediate storage
+.grunt
+
+# Bower dependency directory
+bower_components/
+
+# Yarn Integrity file
+.yarn-integrity
+
+# Yarn 2/3
+.yarn/*
+!.yarn/cache
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+.pnp.*
+.yarnrc.yml
+
+# pnpm
+node_modules/.pnpm-store/
+pnpm-lock.yaml
+
+# Turbo
+.turbo/
+
+# -------------------------------------------------------------------
+# REACT NATIVE SPECIFIC
+# -------------------------------------------------------------------
+.expo/
+.expo-shared/
+.expo-cache/
+.expo-web-cache/
+
+# Metro bundler cache
+.rn-cache/
+.metrobundler/
+.metro/
+metro.config.js.timestamp
+metro-cache/
+.haste-map-*
+.haste-cache/
+
+# React Native CLI
+.react-native/
+.rncli/
+
+# Build artifacts
+.release-please-manifest.json
+.release-it.json
+.changelog/
+
+# Hermes debugger
+.hermes/
+
+# Fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+
+# -------------------------------------------------------------------
+# ANDROID (Comprehensive)
+# -------------------------------------------------------------------
+# Build
+android/**/build/
+android/**/.cxx/
+android/**/.gradle/
+android/**/.idea/
+android/**/local.properties
+android/**/*.iml
+android/**/*.iws
+android/**/*.ipr
+
+# Generated files
+android/**/generated/
+android/**/intermediates/
+android/**/outputs/
+android/**/tmp/
+android/**/.apt_generated/
+
+# Keystores and certificates
+android/**/*.keystore
+android/**/*.jks
+android/**/*.pk8
+android/**/*.pem
+android/**/*.der
+android/**/*.pfx
+android/**/*.p12
+android/**/debug.keystore
+android/**/release.keystore
+
+# Gradle
+android/**/.gradle/
+android/**/gradle/
+android/**/gradlew
+android/**/gradlew.bat
+android/**/gradle/wrapper/gradle-wrapper.jar
+android/**/gradle/wrapper/gradle-wrapper.properties
+android/**/caches/
+android/**/wrapper/dists/
+
+# Android Studio
+android/**/.android/
+android/**/captures/
+android/**/*.hprof
+
+# NDK
+android/**/.externalNativeBuild/
+android/**/obj/
+android/**/libs/
+android/**/jniLibs/
+android/**/*.so
+android/**/*.o
+android/**/*.a
+
+# Autolinking
+android/**/src/main/java/com/facebook/react/
+android/**/src/main/jni/
+
+# CMake
+android/**/.cxx/
+android/**/CMakeFiles/
+android/**/CMakeCache.txt
+android/**/cmake_install.cmake
+android/**/Makefile
+android/**/*.ninja
+android/**/.ninja_deps
+android/**/.ninja_log
+android/**/compile_commands.json
+
+# -------------------------------------------------------------------
+# iOS (Comprehensive)
+# -------------------------------------------------------------------
+# Build
+ios/**/build/
+ios/**/DerivedData/
+ios/**/xcuserdata/
+ios/**/.xcode/
+
+# Xcode
+*.xcodeproj/*
+!*.xcodeproj/project.pbxproj
+!*.xcodeproj/xcshareddata/
+!*.xcodeproj/xcshareddata/xcschemes/
+*.xcodeproj/xcuserdata/
+*.xcodeproj/project.xcworkspace/xcuserdata/
+*.xcodeproj/xcshareddata/IDEWorkspaceChecks.plist
+
+# Xcode workspaces
+*.xcworkspace/*
+!*.xcworkspace/contents.xcworkspacedata
+*.xcworkspace/xcuserdata/
+
+# CocoaPods
+ios/**/Pods/
+Podfile.lock
+ios/**/Podfile.lock
+ios/**/Manifest.lock
+ios/**/Podfile.local
+
+# Carthage
+Carthage/
+ios/**/Carthage/
+
+# Swift Package Manager
+.swiftpm/
+Package.resolved
+ios/**/.build/
+ios/**/.swiftpm/
+
+# Swift/Objective-C precompiled headers
+*.pch
+*.gch
+
+# Compiled sources
+*.o
+*.obj
+*.dylib
+*.framework
+*.app
+*.ipa
+*.dSYM
+*.bcsymbolmap
+
+# App-specific
+ios/**/app-tvos.app
+ios/**/app-tvostests.xctest
+ios/**/app.app
+ios/**/appTests.xctest
+ios/**/*.mode1v3
+ios/**/*.mode2v3
+ios/**/*.moved-aside
+ios/**/*.pbxuser
+!ios/**/default.pbxuser
+ios/**/*.perspectivev3
+
+# Playgrounds
+*.playground/
+
+# SwiftLint
+.swiftlint.yml
+
+# -------------------------------------------------------------------
+# WEB PLATFORM
+# -------------------------------------------------------------------
+# Next.js
+.next/
+out/
+.next/cache/
+
+# Nuxt.js
+.nuxt/
+.nuxt-prod/
+
+# Gatsby
+.cache/
+public
+
+# Angular
+dist/
+aot/
+**/node_modules/
+
+# Vue
+dist/
+
+# Svelte
+.svelte-kit/
+/build/
+
+# Parcel
+.parcel-cache/
+dist/
+
+# Webpack
+.webpack/
+webpack-assets.json
+stats.json
+
+# Rollup
+rollup.config-*.js
+
+# Vite
+dist/
+*.local
+
+# Serverless
+.serverless/
+.serverless_nextjs/
+
+# -------------------------------------------------------------------
+# TESTING AND CODE QUALITY
+# -------------------------------------------------------------------
+# Jest
+coverage/
+.nyc_output/
+*.lcov
+jest-coverage/
+__snapshots__/
+*.snap
+
+# Cypress
+cypress/videos/
+cypress/screenshots/
+cypress/downloads/
+cypress/fixtures/example.json
+
+# Playwright
+playwright-report/
+test-results/
+playwright/.cache/
+
+# Detox
+artifacts/
+*.detoxrc.js
+
+# Code coverage
+.clover
+cobertura.xml
+*.gcda
+*.gcno
+*.lcov
+
+# Linters
+.eslintcache
+.stylelintcache
+.tsbuildinfo
+
+# -------------------------------------------------------------------
+# TYPE/FLOW/JAVASCRIPT COMPILATION
+# -------------------------------------------------------------------
+# TypeScript
+*.tsbuildinfo
+dist-tsc/
+.build/
+*.d.ts
+*.js.map
+*.ts.map
+
+# Flow
+.flowconfig
+.flow-typed/
+
+# Babel
+*.babelcache
+lib-cov/
+
+# SWC
+.swc/
+
+# -------------------------------------------------------------------
+# ENVIRONMENT AND CONFIGURATION
+# -------------------------------------------------------------------
+# Environment variables
+.env
+.env.*
+!.env.example
+.env.local
+.env.development
+.env.production
+.env.test
+.env.staging
+.env.ci
+
+# Firebase
+.firebase/
+.firebaserc
+
+# Docker
+.dockerignore
+docker-compose.override.yml
+docker-compose.*.yml
+docker-compose.yml
+
+# CI/CD
+.circleci/config.yml
+.github/workflows/
+.gitlab-ci.yml
+.jenkins/
+.travis.yml
+.azure-pipelines/
+appveyor.yml
+.codeship/
+.shippable.yml
+.semaphore/
+.buildkite/
+
+# -------------------------------------------------------------------
+# DATABASES AND STORAGE
+# -------------------------------------------------------------------
+# SQLite
+*.db
+*.db-journal
+*.sqlite
+*.sqlite3
+
+# Realm
+*.realm
+*.realm.*
+
+# WatermelonDB
+*.watermelondb
+
+# PouchDB
+_pouch_
+
+# LocalStorage backups
+*.localstorage
+*.localstorage-journal
+
+# -------------------------------------------------------------------
+# MEDIA AND ASSETS (GENERATED/COMPRESSED)
+# -------------------------------------------------------------------
+# Images (generated/compressed)
+*.jpg
+*.jpeg
+*.png
+*.gif
+*.webp
+*.ico
+*.bmp
+*.tiff
+
+# Videos
+*.mp4
+*.mov
+*.avi
+*.mkv
+
+# Audio
+*.mp3
+*.wav
+*.ogg
+
+# Fonts (generated)
+*.ttf
+*.otf
+*.woff
+*.woff2
+*.eot
+
+# Archives
+*.zip
+*.tar
+*.gz
+*.bz2
+*.7z
+*.rar
+
+# -------------------------------------------------------------------
+# DOCUMENTATION
+# -------------------------------------------------------------------
+_site/
+docs/_build/
+docs/_site/
+docs/api/
+docs/coverage/
+docs/build/
+docs/dist/
+
+# MkDocs
+site/
+
+# JSDoc
+out-jsdoc/
+
+# Docusaurus
+.docusaurus/
+build/
+
+# -------------------------------------------------------------------
+# PACKAGE MANAGERS AND LOCKFILES
+# -------------------------------------------------------------------
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+shrinkwrap.yaml
+npm-shrinkwrap.json
+
+# Ruby
+Gemfile.lock
+.bundle/
+vendor/bundle/
+
+# Python
+Pipfile.lock
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.python-version
+
+# Go
+go.sum
+go.mod
+vendor/
+
+# Rust
+Cargo.lock
+target/
+
+# -------------------------------------------------------------------
+# LOGS AND TEMPORARY FILES
+# -------------------------------------------------------------------
+logs/
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+firebase-debug.log*
+metro-bundler-*.log
+
+# Temporary files
+tmp/
+temp/
+*.tmp
+*.temp
+*.bak
+*.backup
+*.old
+
+# Crash logs
+*.crash
+*.error
+*.dump
+
+# Performance logs
+*.perf
+*.profile
+*.heapsnapshot
+
+# -------------------------------------------------------------------
+# MISC DEVELOPMENT FILES
+# -------------------------------------------------------------------
+# Storybook
+.storybook/
+storybook-static/
+
+# Chromatic
+.chromatic/
+
+# Sentry
+.sentry/
+.sentryclirc
+
+# Mixpanel
+.mixpanel/
+
+# Analytics
+.amplify/
+.aws/
+
+# ML/AI
+*.pt
+*.pth
+*.h5
+*.keras
+.tensorflow/
+
+# -------------------------------------------------------------------
+# EXCEPTIONS (MUST BE KEPT IN REPO)
+# -------------------------------------------------------------------
+# IMPORTANT: These files SHOULD be committed
+!android/app/src/main/assets/
+!android/app/src/main/res/
+!android/app/src/main/java/com/app/
+!android/app/src/main/AndroidManifest.xml
+!android/app/src/main/res/values/strings.xml
+!android/app/src/main/res/values/styles.xml
+!android/app/build.gradle
+!android/build.gradle
+!android/settings.gradle
+!android/gradle.properties
+!android/gradlew
+!android/gradlew.bat
+!android/gradle/wrapper/gradle-wrapper.jar
+!android/gradle/wrapper/gradle-wrapper.properties
+
+!ios/app/
+!ios/App/
+!ios/app.xcodeproj/project.pbxproj
+!ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme
+!ios/app/AppDelegate.swift
+!ios/app/Info.plist
+!ios/app/Assets.xcassets/
+!ios/app/LaunchScreen.storyboard
+!ios/Podfile
+
+!src/
+!App.js
+!App.tsx
+!index.js
+!index.ts
+!app.json
+!babel.config.js
+!metro.config.js
+!jest.config.js
+!tsconfig.json
+!.eslintrc.js
+!.prettierrc.js
+!.watchmanconfig
+!.bundle/config
+!Gemfile
+!__tests__/
+!package.json
+
+!README.md
+!LICENSE
+!CHANGELOG.md
+!CONTRIBUTING.md
+!SECURITY.md
+!CODE_OF_CONDUCT.md
+
+# Git itself (but allow .git directory)
+!.git/
+!.git/**
+
+# -------------------------------------------------------------------
+# PRODUCTION-SPECIFIC IGNORES
+# -------------------------------------------------------------------
+# Source maps (can be generated during build)
+*.map
+
+# Bundle files (generated during build)
+*.bundle
+*.jsbundle
+
+# Hermes bytecode
+*.hbc
+
+# App binaries
+*.apk
+*.aab
+*.ipa
+*.app
+*.dmg
+
+# App signing
+*.mobileprovision
+*.p12
+*.cer
+
+# Crashlytics
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Google Services
+google-services.json
+GoogleService-Info.plist
+
+# Microsoft App Center
+appcenter-config.json
+
+# OneSignal
+OneSignal.plugin
+
+# RevenueCat
+RevenueCat.plugin
+
+# -------------------------------------------------------------------
+# SECURITY-CRITICAL FILES (NEVER COMMIT)
+# -------------------------------------------------------------------
+# API Keys
+*.apikey
+*.apisecret
+*.secret
+*.token
+*.credential
+
+# SSH Keys
+*.pem
+*.key
+*.pub
+id_rsa
+id_dsa
+*.ppk
+
+# Certificates
+*.crt
+*.cert
+*.pfx
+*.p12
+
+# Passwords
+*.password
+*.pass
+*.pwd
+
+# Configuration with secrets
+config.local.*
+secret.*
+private.*
+prod.*
+staging.*
diff --git a/rn/app/.prettierrc.js b/rn/app/.prettierrc.js
new file mode 100644
index 0000000..3b7074d
--- /dev/null
+++ b/rn/app/.prettierrc.js
@@ -0,0 +1,8 @@
+module.exports = {
+ arrowParens: 'avoid',
+ singleQuote: true,
+ trailingComma: 'none',
+ "useTabs": false,
+ "tabWidth": 4,
+ "printWidth": 150,
+};
diff --git a/rn/app/.watchmanconfig b/rn/app/.watchmanconfig
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/rn/app/.watchmanconfig
@@ -0,0 +1 @@
+{}
diff --git a/rn/app/App.js b/rn/app/App.js
new file mode 100644
index 0000000..ac0e62e
--- /dev/null
+++ b/rn/app/App.js
@@ -0,0 +1,49 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Корневой файл приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const AppErrorBoundary = require('./src/components/layout/AppErrorBoundary'); //Обработчик ошибок
+const AppMessagingProvider = require('./src/components/layout/AppMessagingProvider').AppMessagingProvider; //Провайдер сообщений
+const AppNavigationProvider = require('./src/components/layout/AppNavigationProvider').AppNavigationProvider; //Провайдер навигации
+const AppModeProvider = require('./src/components/layout/AppModeProvider').AppModeProvider; //Провайдер режима работы
+const AppLocalDbProvider = require('./src/components/layout/AppLocalDbProvider').AppLocalDbProvider; //Провайдер локальной БД
+const AppServerProvider = require('./src/components/layout/AppServerProvider').AppServerProvider; //Провайдер сервера
+const AppPreTripInspectionsProvider = require('./src/components/layout/AppPreTripInspectionsProvider').AppPreTripInspectionsProvider; //Провайдер осмотров
+const AppRoot = require('./src/components/layout/AppRoot'); //Корневой layout приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Основное приложение
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = App;
diff --git a/rn/app/Gemfile b/rn/app/Gemfile
new file mode 100644
index 0000000..6a4c5f1
--- /dev/null
+++ b/rn/app/Gemfile
@@ -0,0 +1,16 @@
+source 'https://rubygems.org'
+
+# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
+ruby ">= 2.6.10"
+
+# Exclude problematic versions of cocoapods and activesupport that causes build failures.
+gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
+gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
+gem 'xcodeproj', '< 1.26.0'
+gem 'concurrent-ruby', '< 1.3.4'
+
+# Ruby 3.4.0 has removed some libraries from the standard library.
+gem 'bigdecimal'
+gem 'logger'
+gem 'benchmark'
+gem 'mutex_m'
diff --git a/rn/app/__tests__/App.test.tsx b/rn/app/__tests__/App.test.tsx
new file mode 100644
index 0000000..e532f70
--- /dev/null
+++ b/rn/app/__tests__/App.test.tsx
@@ -0,0 +1,13 @@
+/**
+ * @format
+ */
+
+import React from 'react';
+import ReactTestRenderer from 'react-test-renderer';
+import App from '../App';
+
+test('renders correctly', async () => {
+ await ReactTestRenderer.act(() => {
+ ReactTestRenderer.create();
+ });
+});
diff --git a/rn/app/android/app/build.gradle b/rn/app/android/app/build.gradle
new file mode 100644
index 0000000..0510375
--- /dev/null
+++ b/rn/app/android/app/build.gradle
@@ -0,0 +1,119 @@
+apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
+apply plugin: "com.facebook.react"
+
+/**
+ * This is the configuration block to customize your React Native Android app.
+ * By default you don't need to apply any configuration, just uncomment the lines you need.
+ */
+react {
+ /* Folders */
+ // The root of your project, i.e. where "package.json" lives. Default is '../..'
+ // root = file("../../")
+ // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
+ // reactNativeDir = file("../../node_modules/react-native")
+ // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
+ // codegenDir = file("../../node_modules/@react-native/codegen")
+ // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
+ // cliFile = file("../../node_modules/react-native/cli.js")
+
+ /* Variants */
+ // The list of variants to that are debuggable. For those we're going to
+ // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
+ // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
+ // debuggableVariants = ["liteDebug", "prodDebug"]
+
+ /* Bundling */
+ // A list containing the node command and its flags. Default is just 'node'.
+ // nodeExecutableAndArgs = ["node"]
+ //
+ // The command to run when bundling. By default is 'bundle'
+ // bundleCommand = "ram-bundle"
+ //
+ // The path to the CLI configuration file. Default is empty.
+ // bundleConfig = file(../rn-cli.config.js)
+ //
+ // The name of the generated asset file containing your JS bundle
+ // bundleAssetName = "MyApplication.android.bundle"
+ //
+ // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
+ // entryFile = file("../js/MyApplication.android.js")
+ //
+ // A list of extra flags to pass to the 'bundle' commands.
+ // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
+ // extraPackagerArgs = []
+
+ /* Hermes Commands */
+ // The hermes compiler command to run. By default it is 'hermesc'
+ // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
+ //
+ // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
+ // hermesFlags = ["-O", "-output-source-map"]
+
+ /* Autolinking */
+ autolinkLibrariesWithApp()
+}
+
+/**
+ * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
+ */
+def enableProguardInReleaseBuilds = false
+
+/**
+ * The preferred build flavor of JavaScriptCore (JSC)
+ *
+ * For example, to use the international variant, you can use:
+ * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
+ *
+ * The international variant includes ICU i18n library and necessary data
+ * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
+ * give correct results when using with locales other than en-US. Note that
+ * this variant is about 6MiB larger per architecture than default.
+ */
+def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
+
+android {
+ ndkVersion rootProject.ext.ndkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ compileSdk rootProject.ext.compileSdkVersion
+
+ namespace "com.app"
+ defaultConfig {
+ applicationId "com.app"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode 1
+ versionName "1.0"
+ }
+ signingConfigs {
+ debug {
+ storeFile file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+ release {
+ // Caution! In production, you need to generate your own keystore file.
+ // see https://reactnative.dev/docs/signed-apk-android.
+ signingConfig signingConfigs.debug
+ minifyEnabled enableProguardInReleaseBuilds
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ }
+ }
+}
+
+dependencies {
+ // The version of react-native is set by the React Native Gradle Plugin
+ implementation("com.facebook.react:react-android")
+
+ if (hermesEnabled.toBoolean()) {
+ implementation("com.facebook.react:hermes-android")
+ } else {
+ implementation jscFlavor
+ }
+}
diff --git a/rn/app/android/app/proguard-rules.pro b/rn/app/android/app/proguard-rules.pro
new file mode 100644
index 0000000..11b0257
--- /dev/null
+++ b/rn/app/android/app/proguard-rules.pro
@@ -0,0 +1,10 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
diff --git a/rn/app/android/app/src/main/AndroidManifest.xml b/rn/app/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..fb78f39
--- /dev/null
+++ b/rn/app/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rn/app/android/app/src/main/java/com/app/MainActivity.kt b/rn/app/android/app/src/main/java/com/app/MainActivity.kt
new file mode 100644
index 0000000..7fa1d6c
--- /dev/null
+++ b/rn/app/android/app/src/main/java/com/app/MainActivity.kt
@@ -0,0 +1,22 @@
+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)
+}
diff --git a/rn/app/android/app/src/main/java/com/app/MainApplication.kt b/rn/app/android/app/src/main/java/com/app/MainApplication.kt
new file mode 100644
index 0000000..05a9022
--- /dev/null
+++ b/rn/app/android/app/src/main/java/com/app/MainApplication.kt
@@ -0,0 +1,27 @@
+package com.app
+
+import android.app.Application
+import com.facebook.react.PackageList
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactHost
+import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
+import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
+
+class MainApplication : Application(), ReactApplication {
+
+ override val reactHost: ReactHost by lazy {
+ getDefaultReactHost(
+ context = applicationContext,
+ packageList =
+ PackageList(this).packages.apply {
+ // Packages that cannot be autolinked yet can be added manually here, for example:
+ // add(MyReactNativePackage())
+ },
+ )
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ loadReactNative(this)
+ }
+}
diff --git a/rn/app/android/app/src/main/res/drawable/rn_edit_text_material.xml b/rn/app/android/app/src/main/res/drawable/rn_edit_text_material.xml
new file mode 100644
index 0000000..5c25e72
--- /dev/null
+++ b/rn/app/android/app/src/main/res/drawable/rn_edit_text_material.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rn/app/android/app/src/main/res/values/strings.xml b/rn/app/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e2f7269
--- /dev/null
+++ b/rn/app/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Парус© ПО
+
diff --git a/rn/app/android/app/src/main/res/values/styles.xml b/rn/app/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..7ba83a2
--- /dev/null
+++ b/rn/app/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/rn/app/android/build.gradle b/rn/app/android/build.gradle
new file mode 100644
index 0000000..dad99b0
--- /dev/null
+++ b/rn/app/android/build.gradle
@@ -0,0 +1,21 @@
+buildscript {
+ ext {
+ buildToolsVersion = "36.0.0"
+ minSdkVersion = 24
+ compileSdkVersion = 36
+ targetSdkVersion = 36
+ ndkVersion = "27.1.12297006"
+ kotlinVersion = "2.1.20"
+ }
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath("com.android.tools.build:gradle")
+ classpath("com.facebook.react:react-native-gradle-plugin")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
+ }
+}
+
+apply plugin: "com.facebook.react.rootproject"
diff --git a/rn/app/android/gradle.properties b/rn/app/android/gradle.properties
new file mode 100644
index 0000000..9afe615
--- /dev/null
+++ b/rn/app/android/gradle.properties
@@ -0,0 +1,44 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+
+# Use this property to specify which architecture you want to build.
+# You can also override it from the CLI using
+# ./gradlew -PreactNativeArchitectures=x86_64
+reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
+
+# Use this property to enable support to the new architecture.
+# This will allow you to use TurboModules and the Fabric render in
+# your application. You should enable this flag either if you want
+# to write custom TurboModules/Fabric components OR use libraries that
+# are providing them.
+newArchEnabled=true
+
+# Use this property to enable or disable the Hermes JS engine.
+# If set to false, you will be using JSC instead.
+hermesEnabled=true
+
+# Use this property to enable edge-to-edge display support.
+# This allows your app to draw behind system bars for an immersive UI.
+# Note: Only works with ReactActivity and should not be used with custom Activity.
+edgeToEdgeEnabled=false
diff --git a/rn/app/android/gradlew b/rn/app/android/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/rn/app/android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/rn/app/android/gradlew.bat b/rn/app/android/gradlew.bat
new file mode 100644
index 0000000..dd2b8ee
--- /dev/null
+++ b/rn/app/android/gradlew.bat
@@ -0,0 +1,99 @@
+@REM Copyright (c) Meta Platforms, Inc. and affiliates.
+@REM
+@REM This source code is licensed under the MIT license found in the
+@REM LICENSE file in the root directory of this source tree.
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/rn/app/android/settings.gradle b/rn/app/android/settings.gradle
new file mode 100644
index 0000000..acd6fda
--- /dev/null
+++ b/rn/app/android/settings.gradle
@@ -0,0 +1,6 @@
+pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
+plugins { id("com.facebook.react.settings") }
+extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
+rootProject.name = 'app'
+include ':app'
+includeBuild('../node_modules/@react-native/gradle-plugin')
diff --git a/rn/app/app.json b/rn/app/app.json
new file mode 100644
index 0000000..f728d4a
--- /dev/null
+++ b/rn/app/app.json
@@ -0,0 +1,4 @@
+{
+ "name": "Pre-Trip_Inspections",
+ "displayName": "Парус© Предрейсовые осмотры"
+}
diff --git a/rn/app/babel.config.js b/rn/app/babel.config.js
new file mode 100644
index 0000000..f7b3da3
--- /dev/null
+++ b/rn/app/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ['module:@react-native/babel-preset'],
+};
diff --git a/rn/app/index.js b/rn/app/index.js
new file mode 100644
index 0000000..b4a1100
--- /dev/null
+++ b/rn/app/index.js
@@ -0,0 +1,20 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Точка входа приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { AppRegistry } = require("react-native"); //Регистрация корневого компонента
+const App = require("./App"); //Корневой компонент приложения
+const { name: appName } = require("./app.json"); //Имя приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Регистрация корневого компонента
+AppRegistry.registerComponent(appName, () => App);
+
diff --git a/rn/app/ios/.xcode.env b/rn/app/ios/.xcode.env
new file mode 100644
index 0000000..3d5782c
--- /dev/null
+++ b/rn/app/ios/.xcode.env
@@ -0,0 +1,11 @@
+# This `.xcode.env` file is versioned and is used to source the environment
+# used when running script phases inside Xcode.
+# To customize your local environment, you can create an `.xcode.env.local`
+# file that is not versioned.
+
+# NODE_BINARY variable contains the PATH to the node executable.
+#
+# Customize the NODE_BINARY variable here.
+# For example, to use nvm with brew, add the following line
+# . "$(brew --prefix nvm)/nvm.sh" --no-use
+export NODE_BINARY=$(command -v node)
diff --git a/rn/app/ios/Podfile b/rn/app/ios/Podfile
new file mode 100644
index 0000000..9632bdb
--- /dev/null
+++ b/rn/app/ios/Podfile
@@ -0,0 +1,34 @@
+# Resolve react_native_pods.rb with node to allow for hoisting
+require Pod::Executable.execute_command('node', ['-p',
+ 'require.resolve(
+ "react-native/scripts/react_native_pods.rb",
+ {paths: [process.argv[1]]},
+ )', __dir__]).strip
+
+platform :ios, min_ios_version_supported
+prepare_react_native_project!
+
+linkage = ENV['USE_FRAMEWORKS']
+if linkage != nil
+ Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
+ use_frameworks! :linkage => linkage.to_sym
+end
+
+target 'app' do
+ config = use_native_modules!
+
+ use_react_native!(
+ :path => config[:reactNativePath],
+ # An absolute path to your application root.
+ :app_path => "#{Pod::Config.instance.installation_root}/.."
+ )
+
+ post_install do |installer|
+ react_native_post_install(
+ installer,
+ config[:reactNativePath],
+ :mac_catalyst_enabled => false,
+ # :ccache_enabled => true
+ )
+ end
+end
diff --git a/rn/app/ios/app.xcodeproj/project.pbxproj b/rn/app/ios/app.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..c9e04ff
--- /dev/null
+++ b/rn/app/ios/app.xcodeproj/project.pbxproj
@@ -0,0 +1,471 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0C80B921A6F3F58F76C31292 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-app.a */; };
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
+ 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
+ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 13B07F961A680F5B00A75B9A /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = app/Images.xcassets; sourceTree = ""; };
+ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = app/Info.plist; sourceTree = ""; };
+ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = app/PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 3B4392A12AC88292D35C810B /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = ""; };
+ 5709B34CF0A7D63546082F79 /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = ""; };
+ 5DCACB8F33CDC322A6C60F78 /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = app/AppDelegate.swift; sourceTree = ""; };
+ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = app/LaunchScreen.storyboard; sourceTree = ""; };
+ ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0C80B921A6F3F58F76C31292 /* libPods-app.a in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 13B07FAE1A68108700A75B9A /* app */ = {
+ isa = PBXGroup;
+ children = (
+ 13B07FB51A68108700A75B9A /* Images.xcassets */,
+ 761780EC2CA45674006654EE /* AppDelegate.swift */,
+ 13B07FB61A68108700A75B9A /* Info.plist */,
+ 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
+ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
+ );
+ name = app;
+ sourceTree = "";
+ };
+ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
+ 5DCACB8F33CDC322A6C60F78 /* libPods-app.a */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 832341AE1AAA6A7D00B99B32 /* Libraries */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Libraries;
+ sourceTree = "";
+ };
+ 83CBB9F61A601CBA00E9B192 = {
+ isa = PBXGroup;
+ children = (
+ 13B07FAE1A68108700A75B9A /* app */,
+ 832341AE1AAA6A7D00B99B32 /* Libraries */,
+ 83CBBA001A601CBA00E9B192 /* Products */,
+ 2D16E6871FA4F8E400B85C8A /* Frameworks */,
+ BBD78D7AC51CEA395F1C20DB /* Pods */,
+ );
+ indentWidth = 2;
+ sourceTree = "";
+ tabWidth = 2;
+ usesTabs = 0;
+ };
+ 83CBBA001A601CBA00E9B192 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 13B07F961A680F5B00A75B9A /* app.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ BBD78D7AC51CEA395F1C20DB /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3B4392A12AC88292D35C810B /* Pods-app.debug.xcconfig */,
+ 5709B34CF0A7D63546082F79 /* Pods-app.release.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 13B07F861A680F5B00A75B9A /* app */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */;
+ buildPhases = (
+ C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,
+ 13B07F871A680F5B00A75B9A /* Sources */,
+ 13B07F8C1A680F5B00A75B9A /* Frameworks */,
+ 13B07F8E1A680F5B00A75B9A /* Resources */,
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
+ 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,
+ E235C05ADACE081382539298 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = app;
+ productName = app;
+ productReference = 13B07F961A680F5B00A75B9A /* app.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 83CBB9F71A601CBA00E9B192 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1210;
+ TargetAttributes = {
+ 13B07F861A680F5B00A75B9A = {
+ LastSwiftMigration = 1120;
+ };
+ };
+ };
+ buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */;
+ compatibilityVersion = "Xcode 12.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 83CBB9F61A601CBA00E9B192;
+ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 13B07F861A680F5B00A75B9A /* app */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 13B07F8E1A680F5B00A75B9A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "$(SRCROOT)/.xcode.env.local",
+ "$(SRCROOT)/.xcode.env",
+ );
+ name = "Bundle React Native code and images";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n";
+ };
+ 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-app-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 13B07F871A680F5B00A75B9A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 13B07F941A680F5B00A75B9A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-app.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = app/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
+ PRODUCT_NAME = app;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 13B07F951A680F5B00A75B9A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-app.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ INFOPLIST_FILE = app/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-ObjC",
+ "-lc++",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
+ PRODUCT_NAME = app;
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+ 83CBBA201A601CBA00E9B192 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "c++20";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ /usr/lib/swift,
+ "$(inherited)",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(SDKROOT)/usr/lib/swift\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ OTHER_CPLUSPLUSFLAGS = (
+ "$(OTHER_CFLAGS)",
+ "-DFOLLY_NO_CONFIG",
+ "-DFOLLY_MOBILE=1",
+ "-DFOLLY_USE_LIBCPP=1",
+ "-DFOLLY_CFG_NO_COROUTINES=1",
+ "-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ );
+ SDKROOT = iphoneos;
+ };
+ name = Debug;
+ };
+ 83CBBA211A601CBA00E9B192 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "c++20";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ /usr/lib/swift,
+ "$(inherited)",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "\"$(SDKROOT)/usr/lib/swift\"",
+ "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
+ "\"$(inherited)\"",
+ );
+ MTL_ENABLE_DEBUG_INFO = NO;
+ OTHER_CPLUSPLUSFLAGS = (
+ "$(OTHER_CFLAGS)",
+ "-DFOLLY_NO_CONFIG",
+ "-DFOLLY_MOBILE=1",
+ "-DFOLLY_USE_LIBCPP=1",
+ "-DFOLLY_CFG_NO_COROUTINES=1",
+ "-DFOLLY_HAVE_CLOCK_GETTIME=1",
+ );
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 13B07F941A680F5B00A75B9A /* Debug */,
+ 13B07F951A680F5B00A75B9A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 83CBBA201A601CBA00E9B192 /* Debug */,
+ 83CBBA211A601CBA00E9B192 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
+}
diff --git a/rn/app/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/rn/app/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme
new file mode 100644
index 0000000..9b78d17
--- /dev/null
+++ b/rn/app/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rn/app/ios/app/AppDelegate.swift b/rn/app/ios/app/AppDelegate.swift
new file mode 100644
index 0000000..d04d5eb
--- /dev/null
+++ b/rn/app/ios/app/AppDelegate.swift
@@ -0,0 +1,48 @@
+import UIKit
+import React
+import React_RCTAppDelegate
+import ReactAppDependencyProvider
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ var window: UIWindow?
+
+ var reactNativeDelegate: ReactNativeDelegate?
+ var reactNativeFactory: RCTReactNativeFactory?
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ let delegate = ReactNativeDelegate()
+ let factory = RCTReactNativeFactory(delegate: delegate)
+ delegate.dependencyProvider = RCTAppDependencyProvider()
+
+ reactNativeDelegate = delegate
+ reactNativeFactory = factory
+
+ window = UIWindow(frame: UIScreen.main.bounds)
+
+ factory.startReactNative(
+ withModuleName: "Pre-Trip_Inspections",
+ in: window,
+ launchOptions: launchOptions
+ )
+
+ return true
+ }
+}
+
+class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
+ override func sourceURL(for bridge: RCTBridge) -> URL? {
+ self.bundleURL()
+ }
+
+ override func bundleURL() -> URL? {
+#if DEBUG
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
+#else
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
+#endif
+ }
+}
diff --git a/rn/app/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json b/rn/app/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..8121323
--- /dev/null
+++ b/rn/app/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,53 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/rn/app/ios/app/Images.xcassets/Contents.json b/rn/app/ios/app/Images.xcassets/Contents.json
new file mode 100644
index 0000000..2d92bd5
--- /dev/null
+++ b/rn/app/ios/app/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/rn/app/ios/app/Info.plist b/rn/app/ios/app/Info.plist
new file mode 100644
index 0000000..0967436
--- /dev/null
+++ b/rn/app/ios/app/Info.plist
@@ -0,0 +1,54 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ Парус© ПО
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Парус© Предрейсовые осмотры
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+
+ NSAllowsArbitraryLoads
+
+ NSAllowsLocalNetworking
+
+
+ NSLocationWhenInUseUsageDescription
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/rn/app/ios/app/LaunchScreen.storyboard b/rn/app/ios/app/LaunchScreen.storyboard
new file mode 100644
index 0000000..61f6b47
--- /dev/null
+++ b/rn/app/ios/app/LaunchScreen.storyboard
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rn/app/ios/app/PrivacyInfo.xcprivacy b/rn/app/ios/app/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..41b8317
--- /dev/null
+++ b/rn/app/ios/app/PrivacyInfo.xcprivacy
@@ -0,0 +1,37 @@
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryUserDefaults
+ NSPrivacyAccessedAPITypeReasons
+
+ CA92.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/rn/app/jest.config.js b/rn/app/jest.config.js
new file mode 100644
index 0000000..8eb675e
--- /dev/null
+++ b/rn/app/jest.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ preset: 'react-native',
+};
diff --git a/rn/app/metro.config.js b/rn/app/metro.config.js
new file mode 100644
index 0000000..2a0a21c
--- /dev/null
+++ b/rn/app/metro.config.js
@@ -0,0 +1,11 @@
+const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
+
+/**
+ * Metro configuration
+ * https://reactnative.dev/docs/metro
+ *
+ * @type {import('@react-native/metro-config').MetroConfig}
+ */
+const config = {};
+
+module.exports = mergeConfig(getDefaultConfig(__dirname), config);
diff --git a/rn/app/package.json b/rn/app/package.json
new file mode 100644
index 0000000..20bf981
--- /dev/null
+++ b/rn/app/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "app",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "android": "react-native run-android",
+ "ios": "react-native run-ios",
+ "lint": "eslint .",
+ "start": "react-native start",
+ "test": "jest"
+ },
+ "dependencies": {
+ "@react-native/new-app-screen": "^0.83.1",
+ "react": "^19.2.0",
+ "react-native": "^0.83.1",
+ "react-native-quick-sqlite": "^8.2.7",
+ "react-native-safe-area-context": "^5.5.2"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/eslint-parser": "^7.28.6",
+ "@babel/plugin-syntax-jsx": "^7.28.6",
+ "@babel/preset-env": "^7.25.3",
+ "@babel/runtime": "^7.25.0",
+ "@react-native-community/cli": "^20.0.0",
+ "@react-native-community/cli-platform-android": "^20.0.0",
+ "@react-native-community/cli-platform-ios": "^20.0.0",
+ "@react-native/babel-preset": "^0.83.1",
+ "@react-native/eslint-config": "^0.83.1",
+ "@react-native/metro-config": "^0.83.1",
+ "@react-native/typescript-config": "^0.83.1",
+ "@types/jest": "^29.5.13",
+ "@types/react": "^19.2.0",
+ "@types/react-test-renderer": "^19.1.0",
+ "eslint": "^8.19.0",
+ "jest": "^29.6.3",
+ "prettier": "^2.8.8",
+ "react-test-renderer": "^19.2.0",
+ "typescript": "^5.8.3"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/rn/app/src/components/common/AdaptiveView.js b/rn/app/src/components/common/AdaptiveView.js
new file mode 100644
index 0000000..111fdb5
--- /dev/null
+++ b/rn/app/src/components/common/AdaptiveView.js
@@ -0,0 +1,47 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Адаптивный контейнер для всех экранов
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View } = require('react-native'); //Базовые компоненты
+const { useSafeAreaInsets } = require('react-native-safe-area-context'); //Отступы безопасной области
+const styles = require('../../styles/common/AdaptiveView.styles'); //Стили адаптивного вида
+
+//-----------
+//Тело модуля
+//-----------
+
+//Адаптивный контейнер для экранов
+function AdaptiveView({ children, style, padding = true, safeArea = true, ...restProps }) {
+ const insets = useSafeAreaInsets();
+
+ //Определяем стиль контейнера с учетом safe area
+ const containerStyle = [
+ styles.container,
+ padding && styles.padding,
+ safeArea && {
+ paddingTop: insets.top,
+ paddingBottom: insets.bottom,
+ paddingLeft: insets.left,
+ paddingRight: insets.right
+ },
+ style
+ ];
+
+ return (
+
+ {children}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AdaptiveView;
diff --git a/rn/app/src/components/common/AppButton.js b/rn/app/src/components/common/AppButton.js
new file mode 100644
index 0000000..db6ee08
--- /dev/null
+++ b/rn/app/src/components/common/AppButton.js
@@ -0,0 +1,42 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Общий компонент кнопки
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { Pressable, View } = require('react-native'); //Базовые компоненты
+const AppText = require('./AppText'); //Общий текстовый компонент
+const styles = require('../../styles/common/AppButton.styles'); //Стили кнопки
+
+//-----------
+//Тело модуля
+//-----------
+
+//Общая кнопка приложения
+function AppButton({ title, onPress, disabled = false, style, textStyle }) {
+ const handlePress = React.useCallback(() => {
+ if (!disabled && typeof onPress === 'function') onPress();
+ }, [disabled, onPress]);
+
+ return (
+ [styles.base, disabled && styles.disabled, pressed && !disabled && styles.pressed, style]}
+ >
+
+ {title}
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppButton;
diff --git a/rn/app/src/components/common/AppInput.js b/rn/app/src/components/common/AppInput.js
new file mode 100644
index 0000000..61b2e9c
--- /dev/null
+++ b/rn/app/src/components/common/AppInput.js
@@ -0,0 +1,80 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Адаптивный компонент ввода
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { TextInput, View } = require('react-native'); //Базовые компоненты
+const AppText = require('./AppText'); //Общий текстовый компонент
+const styles = require('../../styles/common/AppInput.styles'); //Стили ввода
+
+//-----------
+//Тело модуля
+//-----------
+
+//Адаптивный компонент ввода
+function AppInput({
+ label,
+ value,
+ onChangeText,
+ placeholder,
+ secureTextEntry = false,
+ keyboardType = 'default',
+ autoCapitalize = 'none',
+ error,
+ helperText,
+ disabled = false,
+ style,
+ inputStyle,
+ labelStyle,
+ ...restProps
+}) {
+ const [isFocused, setIsFocused] = React.useState(false);
+
+ const handleFocus = () => setIsFocused(true);
+ const handleBlur = () => setIsFocused(false);
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+
+
+ {(error || helperText) && (
+
+ {error || helperText}
+
+ )}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppInput;
diff --git a/rn/app/src/components/common/AppLogo.js b/rn/app/src/components/common/AppLogo.js
new file mode 100644
index 0000000..fc2f785
--- /dev/null
+++ b/rn/app/src/components/common/AppLogo.js
@@ -0,0 +1,72 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент иконки/логотипа приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View } = require('react-native'); //Базовые компоненты
+const styles = require('../../styles/common/AppLogo.styles'); //Стили логотипа
+
+//-----------
+//Тело модуля
+//-----------
+
+//Иконка/логотип приложения
+function AppLogo({ size = 'medium', style }) {
+ //Выбор стилей в зависимости от размера
+ const getSizeStyles = React.useCallback(() => {
+ switch (size) {
+ case 'small':
+ return {
+ container: styles.containerSmall,
+ head: styles.headSmall,
+ eye: styles.eyeSmall,
+ antenna: styles.antennaSmall
+ };
+ case 'large':
+ return {
+ container: styles.containerLarge,
+ head: styles.headLarge,
+ eye: styles.eyeLarge,
+ antenna: styles.antennaLarge
+ };
+ default:
+ return {
+ container: styles.containerMedium,
+ head: styles.headMedium,
+ eye: styles.eyeMedium,
+ antenna: styles.antennaMedium
+ };
+ }
+ }, [size]);
+
+ const sizeStyles = getSizeStyles();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppLogo;
diff --git a/rn/app/src/components/common/AppMessage.js b/rn/app/src/components/common/AppMessage.js
new file mode 100644
index 0000000..e658867
--- /dev/null
+++ b/rn/app/src/components/common/AppMessage.js
@@ -0,0 +1,140 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Общий компонент сообщения приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require("react"); //React и хуки
+const { Modal, View, Text, Pressable } = require("react-native"); //Базовые компоненты
+const styles = require("../../styles/common/AppMessage.styles"); //Стили сообщения
+
+//---------
+//Константы
+//---------
+
+//Типы сообщений
+const APP_MESSAGE_VARIANT = {
+ INFO: "INFO",
+ WARN: "WARN",
+ ERR: "ERR",
+ SUCCESS: "SUCCESS"
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Кнопка сообщения
+function AppMessageButton({ title, onPress, onDismiss, buttonStyle, textStyle }) {
+ //Обработчик нажатия - вызывает onPress и закрывает диалог
+ const handlePress = React.useCallback(() => {
+ //Сначала закрываем диалог
+ if (typeof onDismiss === "function") {
+ onDismiss();
+ }
+
+ //Затем выполняем действие кнопки
+ if (typeof onPress === "function") {
+ onPress();
+ }
+ }, [onPress, onDismiss]);
+
+ return (
+ [styles.buttonBase, pressed && styles.buttonPressed, buttonStyle]} onPress={handlePress}>
+ {title}
+
+ );
+}
+
+//Сообщение приложения
+function AppMessage({
+ visible,
+ variant = APP_MESSAGE_VARIANT.INFO,
+ title,
+ message,
+ //Кнопки: массив объектов { id, title, onPress, buttonStyle, textStyle }
+ buttons,
+ //Обработчик закрытия по "крестику" или по системному Back на Android
+ onRequestClose,
+ //Кастомизация стилей контейнера и содержимого
+ containerStyle,
+ contentStyle,
+ titleStyle,
+ messageStyle,
+ headerStyle
+}) {
+ //Флаг наличия кнопок
+ const hasButtons = Array.isArray(buttons) && buttons.length > 0;
+
+ //Подбор варианта оформления по типу сообщения
+ const containerVariantStyle =
+ variant === APP_MESSAGE_VARIANT.ERR
+ ? styles.containerError
+ : variant === APP_MESSAGE_VARIANT.WARN
+ ? styles.containerWarn
+ : variant === APP_MESSAGE_VARIANT.SUCCESS
+ ? styles.containerSuccess
+ : styles.containerInfo;
+
+ //Обработчик закрытия
+ const handleClose = React.useCallback(() => {
+ if (typeof onRequestClose === "function") onRequestClose();
+ }, [onRequestClose]);
+
+ return (
+
+
+
+
+
+ {title || ""}
+
+
+ ×
+
+
+
+ {message}
+
+ {hasButtons ? (
+
+ {buttons.map(btn => (
+
+ ))}
+
+ ) : null}
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppMessage,
+ APP_MESSAGE_VARIANT
+};
+
diff --git a/rn/app/src/components/common/AppText.js b/rn/app/src/components/common/AppText.js
new file mode 100644
index 0000000..9513aca
--- /dev/null
+++ b/rn/app/src/components/common/AppText.js
@@ -0,0 +1,31 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Общий компонент текста
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { Text } = require('react-native'); //Базовый компонент текста
+const styles = require('../../styles/common/AppText.styles'); //Стили текста
+
+//-----------
+//Тело модуля
+//-----------
+
+//Общий текстовый компонент приложения
+function AppText({ style, children, ...restProps }) {
+ return (
+
+ {children}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppText;
diff --git a/rn/app/src/components/common/Backdrop.js b/rn/app/src/components/common/Backdrop.js
new file mode 100644
index 0000000..a02262f
--- /dev/null
+++ b/rn/app/src/components/common/Backdrop.js
@@ -0,0 +1,41 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент заднего фона (оверлей)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { Pressable } = require('react-native'); //Базовые компоненты
+const styles = require('../../styles/common/Backdrop.styles'); //Стили заднего фона
+
+//-----------
+//Тело модуля
+//-----------
+
+//Задний фон для модальных окон и меню
+function Backdrop({ visible, onPress, style, children }) {
+ const handlePress = React.useCallback(() => {
+ if (typeof onPress === 'function') {
+ onPress();
+ }
+ }, [onPress]);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = Backdrop;
diff --git a/rn/app/src/components/common/CopyButton.js b/rn/app/src/components/common/CopyButton.js
new file mode 100644
index 0000000..bc41cb0
--- /dev/null
+++ b/rn/app/src/components/common/CopyButton.js
@@ -0,0 +1,69 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент кнопки копирования в буфер обмена
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View, Pressable } = require('react-native'); //Базовые компоненты
+const { copyToClipboard } = require('../../utils/clipboard'); //Утилита буфера обмена
+const styles = require('../../styles/common/CopyButton.styles'); //Стили кнопки копирования
+
+//-----------
+//Тело модуля
+//-----------
+
+//Иконка копирования (два прямоугольника)
+function CopyIcon() {
+ return (
+
+
+
+
+ );
+}
+
+//Кнопка копирования в буфер обмена
+function CopyButton({ value, onCopy, onError, disabled = false, style }) {
+ //Обработчик нажатия
+ const handlePress = React.useCallback(async () => {
+ if (disabled || !value) {
+ return;
+ }
+
+ try {
+ await copyToClipboard(String(value));
+
+ if (typeof onCopy === 'function') {
+ onCopy(value);
+ }
+ } catch (error) {
+ console.error('Ошибка копирования в буфер:', error);
+
+ if (typeof onError === 'function') {
+ onError(error);
+ }
+ }
+ }, [value, disabled, onCopy, onError]);
+
+ return (
+ [styles.button, pressed && !disabled && styles.buttonPressed, disabled && styles.buttonDisabled, style]}
+ onPress={handlePress}
+ disabled={disabled}
+ >
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = CopyButton;
diff --git a/rn/app/src/components/common/InputDialog.js b/rn/app/src/components/common/InputDialog.js
new file mode 100644
index 0000000..1329008
--- /dev/null
+++ b/rn/app/src/components/common/InputDialog.js
@@ -0,0 +1,156 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент модального окна с полем ввода
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { Modal, View, TextInput, Pressable } = require('react-native'); //Базовые компоненты
+const AppText = require('./AppText'); //Общий текстовый компонент
+const AppButton = require('./AppButton'); //Кнопка
+const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
+
+//-----------
+//Тело модуля
+//-----------
+
+//Модальное окно с полем ввода
+function InputDialog({
+ visible,
+ title = 'Ввод данных',
+ label,
+ value = '',
+ placeholder,
+ keyboardType = 'default',
+ autoCapitalize = 'none',
+ confirmText = 'Сохранить',
+ cancelText = 'Отмена',
+ onConfirm,
+ onCancel,
+ validator,
+ errorMessage
+}) {
+ //Локальное значение для редактирования
+ const [inputValue, setInputValue] = React.useState(value);
+ const [error, setError] = React.useState('');
+ const [isFocused, setIsFocused] = React.useState(false);
+
+ //Сброс значения при открытии диалога
+ React.useEffect(() => {
+ if (visible) {
+ setInputValue(value);
+ setError('');
+ }
+ }, [visible, value]);
+
+ //Обработчик фокуса
+ const handleFocus = React.useCallback(() => {
+ setIsFocused(true);
+ }, []);
+
+ //Обработчик потери фокуса
+ const handleBlur = React.useCallback(() => {
+ setIsFocused(false);
+ }, []);
+
+ //Обработчик изменения текста
+ const handleChangeText = React.useCallback(text => {
+ setInputValue(text);
+ setError('');
+ }, []);
+
+ //Валидация введённого значения
+ const validateInput = React.useCallback(() => {
+ if (typeof validator === 'function') {
+ const validationResult = validator(inputValue);
+ if (validationResult !== true) {
+ setError(validationResult || errorMessage || 'Некорректное значение');
+ return false;
+ }
+ }
+ return true;
+ }, [inputValue, validator, errorMessage]);
+
+ //Обработчик подтверждения
+ const handleConfirm = React.useCallback(() => {
+ if (!validateInput()) {
+ return;
+ }
+
+ if (typeof onConfirm === 'function') {
+ onConfirm(inputValue.trim());
+ }
+ }, [inputValue, validateInput, onConfirm]);
+
+ //Обработчик отмены
+ const handleCancel = React.useCallback(() => {
+ if (typeof onCancel === 'function') {
+ onCancel();
+ }
+ }, [onCancel]);
+
+ //Обработчик закрытия по кнопке "Назад" (Android)
+ const handleRequestClose = React.useCallback(() => {
+ handleCancel();
+ }, [handleCancel]);
+
+ return (
+
+
+
+
+ {title}
+
+ ×
+
+
+
+
+ {label ? (
+
+ {label}
+
+ ) : null}
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = InputDialog;
diff --git a/rn/app/src/components/inspections/InspectionItem.js b/rn/app/src/components/inspections/InspectionItem.js
new file mode 100644
index 0000000..4e7262b
--- /dev/null
+++ b/rn/app/src/components/inspections/InspectionItem.js
@@ -0,0 +1,37 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Элемент списка предрейсовых осмотров
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require("react"); //React
+const { View } = require("react-native"); //Базовые компоненты
+const AppText = require("../common/AppText"); //Общий текстовый компонент
+const styles = require("../../styles/inspections/InspectionItem.styles"); //Стили элемента
+
+//-----------
+//Тело модуля
+//-----------
+
+//Элемент списка осмотров
+function InspectionItem({ item }) {
+ return (
+
+ {item.title}
+
+ Статус: {item.status}
+ Создан: {item.createdAt}
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = InspectionItem;
+
diff --git a/rn/app/src/components/inspections/InspectionList.js b/rn/app/src/components/inspections/InspectionList.js
new file mode 100644
index 0000000..e1d2f32
--- /dev/null
+++ b/rn/app/src/components/inspections/InspectionList.js
@@ -0,0 +1,68 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Список предрейсовых осмотров
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { ActivityIndicator, FlatList, RefreshControl, View } = require('react-native'); //Базовые компоненты списка
+const AppText = require('../common/AppText'); //Общий текстовый компонент
+const AppButton = require('../common/AppButton'); //Общая кнопка
+const InspectionItem = require('./InspectionItem'); //Элемент списка
+const styles = require('../../styles/inspections/InspectionList.styles'); //Стили списка
+
+//-----------
+//Тело модуля
+//-----------
+
+//Список осмотров
+function InspectionList({ inspections, isLoading, error, onRefresh }) {
+ const hasData = Array.isArray(inspections) && inspections.length > 0;
+
+ const renderItem = React.useCallback(({ item }) => , []);
+
+ const keyExtractor = React.useCallback(item => item.id, []);
+
+ const handleRefresh = React.useCallback(() => {
+ if (typeof onRefresh === 'function') onRefresh();
+ }, [onRefresh]);
+
+ if (!hasData && isLoading) {
+ return (
+
+
+ Загружаем данные...
+
+ );
+ }
+
+ if (!hasData && !isLoading) {
+ return (
+
+ Нет данных предрейсовых осмотров
+ {error ? {error} : null}
+
+
+
+
+ );
+ }
+
+ return (
+ }
+ />
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = InspectionList;
diff --git a/rn/app/src/components/layout/AppErrorBoundary.js b/rn/app/src/components/layout/AppErrorBoundary.js
new file mode 100644
index 0000000..93a44ed
--- /dev/null
+++ b/rn/app/src/components/layout/AppErrorBoundary.js
@@ -0,0 +1,88 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Обёртка приложения для обработки ошибок
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View } = require('react-native'); //Базовые компоненты
+const AppText = require('../common/AppText'); //Текстовый компонент
+const AppButton = require('../common/AppButton'); //Кнопка
+const styles = require('../../styles/layout/AppErrorBoundary.styles'); //Стили компонента
+
+//-----------
+//Тело модуля
+//-----------
+
+//Компонент страницы ошибки
+function AppErrorPage({ error, onReload }) {
+ const message = error && error.message ? String(error.message) : 'Произошла непредвиденная ошибка приложения.';
+
+ const handleReload = () => {
+ if (typeof onReload === 'function') {
+ onReload();
+ return;
+ }
+ //Попытка перезагрузки для Web
+ if (typeof window !== 'undefined' && window.location && typeof window.location.reload === 'function') {
+ window.location.reload();
+ }
+ };
+
+ return (
+
+
+ Ошибка приложения
+ {message}
+
+
+
+
+
+ );
+}
+
+//Классический Error Boundary
+class AppErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ hasError: false,
+ error: null
+ };
+ }
+
+ static getDerivedStateFromError(error) {
+ return {
+ hasError: true,
+ error
+ };
+ }
+
+ componentDidCatch(error, info) {
+ //TODO: логгер
+ }
+
+ handleReset = () => {
+ this.setState({
+ hasError: false,
+ error: null
+ });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ return ;
+ }
+ return this.props.children;
+ }
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppErrorBoundary;
diff --git a/rn/app/src/components/layout/AppHeader.js b/rn/app/src/components/layout/AppHeader.js
new file mode 100644
index 0000000..8dfb8a7
--- /dev/null
+++ b/rn/app/src/components/layout/AppHeader.js
@@ -0,0 +1,185 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент заголовка приложения с меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View, Pressable } = require('react-native'); //Базовые компоненты
+const AppText = require('../common/AppText'); //Общий текстовый компонент
+const AppLogo = require('../common/AppLogo'); //Логотип приложения
+const { useAppModeContext } = require('./AppModeProvider'); //Контекст режима работы
+const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
+const { useAppMessagingContext } = require('./AppMessagingProvider'); //Контекст сообщений
+const { getModeLabel, getModeDescription } = require('../../utils/appInfo'); //Утилиты информации
+const styles = require('../../styles/layout/AppHeader.styles'); //Стили заголовка
+
+//-----------
+//Тело модуля
+//-----------
+
+//Индикатор режима работы
+function ModeIndicator({ mode, onPress }) {
+ //Получение конфигурации стилей для режима
+ const getModeStyleConfig = React.useCallback(() => {
+ switch (mode) {
+ case 'ONLINE':
+ return {
+ color: styles.modeOnline,
+ textColor: styles.modeTextOnline
+ };
+ case 'OFFLINE':
+ return {
+ color: styles.modeOffline,
+ textColor: styles.modeTextOffline
+ };
+ case 'NOT_CONNECTED':
+ return {
+ color: styles.modeNotConnected,
+ textColor: styles.modeTextNotConnected
+ };
+ default:
+ return {
+ color: styles.modeUnknown,
+ textColor: styles.modeTextUnknown
+ };
+ }
+ }, [mode]);
+
+ const styleConfig = getModeStyleConfig();
+ const label = getModeLabel(mode);
+
+ return (
+ [styles.modeContainer, styleConfig.color, pressed && styles.modePressed]} onPress={onPress}>
+ {label}
+
+ );
+}
+
+//Иконка стрелки назад
+function BackArrowIcon() {
+ return (
+
+
+
+
+ );
+}
+
+//Кнопка назад
+function BackButton({ onPress }) {
+ return (
+ [styles.backButton, pressed && styles.backButtonPressed]}
+ onPress={onPress}
+ >
+
+
+ );
+}
+
+//Заголовок приложения
+function AppHeader({
+ title,
+ subtitle,
+ showMenuButton = true,
+ onMenuPress,
+ showModeIndicator = true,
+ showBackButton = false,
+ onBackPress
+}) {
+ const { mode } = useAppModeContext();
+ const { currentScreen, SCREENS } = useAppNavigationContext();
+ const { showInfo } = useAppMessagingContext();
+
+ //Получение заголовка экрана
+ const getTitle = React.useCallback(() => {
+ if (title) return title;
+
+ switch (currentScreen) {
+ case SCREENS.MAIN:
+ return 'Парус© Предрейсовые осмотры';
+ case SCREENS.SETTINGS:
+ return 'Настройки';
+ default:
+ return 'Парус© Предрейсовые осмотры';
+ }
+ }, [title, currentScreen, SCREENS.MAIN, SCREENS.SETTINGS]);
+
+ //Получение подзаголовка экрана
+ const getSubtitle = React.useCallback(() => {
+ if (subtitle) return subtitle;
+
+ switch (currentScreen) {
+ case SCREENS.MAIN:
+ return mode === 'NOT_CONNECTED' ? 'Требуется настройка сервера' : 'Демонстрационный экран';
+ case SCREENS.SETTINGS:
+ return 'Конфигурация приложения';
+ default:
+ return '';
+ }
+ }, [subtitle, currentScreen, mode, SCREENS.MAIN, SCREENS.SETTINGS]);
+
+ //Обработчик нажатия на индикатор режима (универсальный для всех экранов)
+ const handleModeIndicatorPress = React.useCallback(() => {
+ const modeLabel = getModeLabel(mode);
+ const modeDescription = getModeDescription(mode);
+
+ showInfo(`Текущий режим: ${modeLabel}`, {
+ message: modeDescription
+ });
+ }, [mode, showInfo]);
+
+ //Отрисовка левой части шапки (логотип или кнопка назад)
+ const renderLeftSection = () => {
+ if (showBackButton) {
+ return ;
+ }
+
+ return ;
+ };
+
+ return (
+
+
+ {renderLeftSection()}
+
+
+
+ {getTitle()}
+
+ {getSubtitle() ? (
+
+ {getSubtitle()}
+
+ ) : null}
+
+
+
+ {showModeIndicator ? : null}
+
+ {showMenuButton ? (
+ [styles.menuButton, pressed && styles.menuButtonPressed]} onPress={onMenuPress}>
+
+
+
+
+
+
+ ) : null}
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppHeader;
diff --git a/rn/app/src/components/layout/AppLocalDbProvider.js b/rn/app/src/components/layout/AppLocalDbProvider.js
new file mode 100644
index 0000000..a4dfe14
--- /dev/null
+++ b/rn/app/src/components/layout/AppLocalDbProvider.js
@@ -0,0 +1,77 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста работы с локальной базой данных
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const useLocalDb = require('../../hooks/useLocalDb'); //Хук локальной БД
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст локальной БД
+const AppLocalDbContext = React.createContext(null);
+
+//Провайдер локальной БД
+function AppLocalDbProvider({ children }) {
+ const api = useLocalDb();
+
+ //Мемоизация значения контекста с перечислением отдельных свойств
+ const value = React.useMemo(
+ () => ({
+ isDbReady: api.isDbReady,
+ inspections: api.inspections,
+ error: api.error,
+ loadInspections: api.loadInspections,
+ saveInspection: api.saveInspection,
+ getSetting: api.getSetting,
+ setSetting: api.setSetting,
+ deleteSetting: api.deleteSetting,
+ getAllSettings: api.getAllSettings,
+ clearSettings: api.clearSettings,
+ clearInspections: api.clearInspections,
+ vacuum: api.vacuum,
+ checkTableExists: api.checkTableExists
+ }),
+ [
+ api.isDbReady,
+ api.inspections,
+ api.error,
+ api.loadInspections,
+ api.saveInspection,
+ api.getSetting,
+ api.setSetting,
+ api.deleteSetting,
+ api.getAllSettings,
+ api.clearSettings,
+ api.clearInspections,
+ api.vacuum,
+ api.checkTableExists
+ ]
+ );
+
+ return {children};
+}
+
+//Хук доступа к контексту локальной БД
+function useAppLocalDbContext() {
+ const ctx = React.useContext(AppLocalDbContext);
+ if (!ctx) {
+ throw new Error('useAppLocalDbContext должен использоваться внутри AppLocalDbProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppLocalDbProvider,
+ useAppLocalDbContext
+};
diff --git a/rn/app/src/components/layout/AppMessagingProvider.js b/rn/app/src/components/layout/AppMessagingProvider.js
new file mode 100644
index 0000000..7c6aef2
--- /dev/null
+++ b/rn/app/src/components/layout/AppMessagingProvider.js
@@ -0,0 +1,93 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста сообщений приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { AppMessage } = require('../common/AppMessage'); //Компонент сообщения
+const useAppMessaging = require('../../hooks/useAppMessaging'); //Хук сообщений
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст сообщений приложения
+const AppMessagingContext = React.createContext(null);
+
+//Провайдер сообщений приложения
+function AppMessagingProvider({ children }) {
+ const messagingApi = useAppMessaging();
+
+ //Деструктурируем необходимые методы и состояние
+ const { hideMessage, state } = messagingApi;
+
+ //Обработчик закрытия окна "крестиком" / Back
+ const handleRequestClose = React.useCallback(() => {
+ hideMessage();
+ }, [hideMessage]);
+
+ //Мемоизация значения контекста с перечислением отдельных свойств
+ const value = React.useMemo(
+ () => ({
+ MSG_TYPE: messagingApi.MSG_TYPE,
+ state: messagingApi.state,
+ showMessage: messagingApi.showMessage,
+ showError: messagingApi.showError,
+ showInfo: messagingApi.showInfo,
+ showWarn: messagingApi.showWarn,
+ showSuccess: messagingApi.showSuccess,
+ hideMessage: messagingApi.hideMessage
+ }),
+ [
+ messagingApi.MSG_TYPE,
+ messagingApi.state,
+ messagingApi.showMessage,
+ messagingApi.showError,
+ messagingApi.showInfo,
+ messagingApi.showWarn,
+ messagingApi.showSuccess,
+ messagingApi.hideMessage
+ ]
+ );
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+//Хук доступа к контексту сообщений приложения
+function useAppMessagingContext() {
+ const ctx = React.useContext(AppMessagingContext);
+ if (!ctx) {
+ throw new Error('useAppMessagingContext должен использоваться внутри AppMessagingProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppMessagingProvider,
+ useAppMessagingContext
+};
diff --git a/rn/app/src/components/layout/AppModeProvider.js b/rn/app/src/components/layout/AppModeProvider.js
new file mode 100644
index 0000000..5303a60
--- /dev/null
+++ b/rn/app/src/components/layout/AppModeProvider.js
@@ -0,0 +1,69 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста режима работы приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const useAppMode = require('../../hooks/useAppMode'); //Хук режима работы
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст режима работы приложения
+const AppModeContext = React.createContext(null);
+
+//Провайдер режима работы приложения
+function AppModeProvider({ children }) {
+ const modeApi = useAppMode();
+
+ //Мемоизация значения контекста с перечислением отдельных свойств
+ const value = React.useMemo(
+ () => ({
+ APP_MODE: modeApi.APP_MODE,
+ mode: modeApi.mode,
+ setMode: modeApi.setMode,
+ setOnline: modeApi.setOnline,
+ setOffline: modeApi.setOffline,
+ setNotConnected: modeApi.setNotConnected,
+ isOnline: modeApi.isOnline,
+ isOffline: modeApi.isOffline,
+ isNotConnected: modeApi.isNotConnected
+ }),
+ [
+ modeApi.APP_MODE,
+ modeApi.mode,
+ modeApi.setMode,
+ modeApi.setOnline,
+ modeApi.setOffline,
+ modeApi.setNotConnected,
+ modeApi.isOnline,
+ modeApi.isOffline,
+ modeApi.isNotConnected
+ ]
+ );
+
+ return {children};
+}
+
+//Хук доступа к контексту режима работы приложения
+function useAppModeContext() {
+ const ctx = React.useContext(AppModeContext);
+ if (!ctx) {
+ throw new Error('useAppModeContext должен использоваться внутри AppModeProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppModeProvider,
+ useAppModeContext
+};
diff --git a/rn/app/src/components/layout/AppNavigationProvider.js b/rn/app/src/components/layout/AppNavigationProvider.js
new file mode 100644
index 0000000..3075ea8
--- /dev/null
+++ b/rn/app/src/components/layout/AppNavigationProvider.js
@@ -0,0 +1,84 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста навигации приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const useAppNavigation = require('../../hooks/useAppNavigation'); //Хук навигации
+const useHardwareBackPress = require('../../hooks/useHardwareBackPress'); //Хук кнопки "Назад"
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст навигации приложения
+const AppNavigationContext = React.createContext(null);
+
+//Провайдер навигации приложения
+function AppNavigationProvider({ children }) {
+ const navigationApi = useAppNavigation();
+
+ //Деструктурируем для использования в useCallback
+ const { canGoBack, goBack } = navigationApi;
+
+ //Обработчик аппаратной кнопки "Назад" (Android)
+ const handleHardwareBackPress = React.useCallback(() => {
+ //Если можно вернуться назад - возвращаемся
+ if (canGoBack) {
+ goBack();
+ return true; //Предотвращаем закрытие приложения
+ }
+
+ //Если нельзя - позволяем системе обработать (закрыть приложение)
+ return false;
+ }, [canGoBack, goBack]);
+
+ //Подключаем обработчик кнопки "Назад"
+ useHardwareBackPress(handleHardwareBackPress, [handleHardwareBackPress]);
+
+ //Мемоизация значения контекста с перечислением отдельных свойств
+ const value = React.useMemo(
+ () => ({
+ SCREENS: navigationApi.SCREENS,
+ currentScreen: navigationApi.currentScreen,
+ screenParams: navigationApi.screenParams,
+ navigate: navigationApi.navigate,
+ goBack: navigationApi.goBack,
+ reset: navigationApi.reset,
+ canGoBack: navigationApi.canGoBack
+ }),
+ [
+ navigationApi.SCREENS,
+ navigationApi.currentScreen,
+ navigationApi.screenParams,
+ navigationApi.navigate,
+ navigationApi.goBack,
+ navigationApi.reset,
+ navigationApi.canGoBack
+ ]
+ );
+
+ return {children};
+}
+
+//Хук доступа к контексту навигации приложения
+function useAppNavigationContext() {
+ const ctx = React.useContext(AppNavigationContext);
+ if (!ctx) {
+ throw new Error('useAppNavigationContext должен использоваться внутри AppNavigationProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppNavigationProvider,
+ useAppNavigationContext
+};
diff --git a/rn/app/src/components/layout/AppPreTripInspectionsProvider.js b/rn/app/src/components/layout/AppPreTripInspectionsProvider.js
new file mode 100644
index 0000000..1bd2be5
--- /dev/null
+++ b/rn/app/src/components/layout/AppPreTripInspectionsProvider.js
@@ -0,0 +1,63 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста предметной области "Предрейсовые осмотры"
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { usePreTripInspections } = require('../../hooks/usePreTripInspections'); //Хук предметной области
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст предрейсовых осмотров
+const AppPreTripInspectionsContext = React.createContext(null);
+
+//Провайдер предрейсовых осмотров
+function AppPreTripInspectionsProvider({ children }) {
+ const inspectionsApi = usePreTripInspections();
+
+ //Мемоизация значения контекста с перечислением отдельных свойств
+ const value = React.useMemo(
+ () => ({
+ inspections: inspectionsApi.inspections,
+ loadStatus: inspectionsApi.loadStatus,
+ error: inspectionsApi.error,
+ isDbReady: inspectionsApi.isDbReady,
+ refreshInspections: inspectionsApi.refreshInspections,
+ upsertInspection: inspectionsApi.upsertInspection
+ }),
+ [
+ inspectionsApi.inspections,
+ inspectionsApi.loadStatus,
+ inspectionsApi.error,
+ inspectionsApi.isDbReady,
+ inspectionsApi.refreshInspections,
+ inspectionsApi.upsertInspection
+ ]
+ );
+
+ return {children};
+}
+
+//Хук доступа к контексту предрейсовых осмотров
+function useAppPreTripInspectionsContext() {
+ const ctx = React.useContext(AppPreTripInspectionsContext);
+ if (!ctx) {
+ throw new Error('useAppPreTripInspectionsContext должен использоваться внутри AppPreTripInspectionsProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppPreTripInspectionsProvider,
+ useAppPreTripInspectionsContext
+};
diff --git a/rn/app/src/components/layout/AppRoot.js b/rn/app/src/components/layout/AppRoot.js
new file mode 100644
index 0000000..2a52aec
--- /dev/null
+++ b/rn/app/src/components/layout/AppRoot.js
@@ -0,0 +1,35 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Корневой layout приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { useColorScheme } = require('react-native'); //Определение темы устройства
+const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
+const AppShell = require('./AppShell'); //Оболочка приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Корневой layout приложения
+function AppRoot() {
+ const colorScheme = useColorScheme();
+ const isDarkMode = colorScheme === 'dark';
+
+ return (
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppRoot;
diff --git a/rn/app/src/components/layout/AppServerProvider.js b/rn/app/src/components/layout/AppServerProvider.js
new file mode 100644
index 0000000..9a8fb9d
--- /dev/null
+++ b/rn/app/src/components/layout/AppServerProvider.js
@@ -0,0 +1,55 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Провайдер контекста работы с сервером приложений
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const useAppServer = require('../../hooks/useAppServer'); //Хук сервера приложений
+
+//-----------
+//Тело модуля
+//-----------
+
+//Контекст работы с сервером
+const AppServerContext = React.createContext(null);
+
+//Провайдер сервера приложений
+function AppServerProvider({ children }) {
+ const api = useAppServer();
+
+ const value = React.useMemo(
+ () => ({
+ executeAction: api.executeAction,
+ abort: api.abort,
+ isRespErr: api.isRespErr,
+ getRespErrMessage: api.getRespErrMessage,
+ RESP_STATUS_OK: api.RESP_STATUS_OK,
+ RESP_STATUS_ERR: api.RESP_STATUS_ERR
+ }),
+ [api.abort, api.executeAction, api.getRespErrMessage, api.isRespErr, api.RESP_STATUS_ERR, api.RESP_STATUS_OK]
+ );
+
+ return {children};
+}
+
+//Хук доступа к контексту сервера приложений
+function useAppServerContext() {
+ const ctx = React.useContext(AppServerContext);
+ if (!ctx) {
+ throw new Error('useAppServerContext должен использоваться внутри AppServerProvider');
+ }
+ return ctx;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ AppServerProvider,
+ useAppServerContext
+};
diff --git a/rn/app/src/components/layout/AppShell.js b/rn/app/src/components/layout/AppShell.js
new file mode 100644
index 0000000..74212c9
--- /dev/null
+++ b/rn/app/src/components/layout/AppShell.js
@@ -0,0 +1,53 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Оболочка приложения (status bar, общая разметка)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { StatusBar, Platform } = require('react-native'); //Базовые компоненты
+const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
+const MainScreen = require('../../screens/MainScreen'); //Главный экран
+const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
+const AdaptiveView = require('../common/AdaptiveView'); //Адаптивный контейнер
+const styles = require('../../styles/layout/AppShell.styles'); //Стили оболочки
+
+//-----------
+//Тело модуля
+//-----------
+
+//Оболочка приложения
+function AppShell({ isDarkMode }) {
+ const { currentScreen, SCREENS } = useAppNavigationContext();
+
+ const renderScreen = React.useCallback(() => {
+ switch (currentScreen) {
+ case SCREENS.MAIN:
+ return ;
+ case SCREENS.SETTINGS:
+ return ;
+ default:
+ return ;
+ }
+ }, [currentScreen, SCREENS.MAIN, SCREENS.SETTINGS]);
+
+ //Определяем цвет status bar в зависимости от темы
+ const statusBarStyle = isDarkMode ? 'light-content' : 'dark-content';
+ const statusBarBackground = isDarkMode ? '#0F172A' : '#F8FAFC';
+
+ return (
+ <>
+
+ {renderScreen()}
+ >
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = AppShell;
diff --git a/rn/app/src/components/menu/EmptyMenu.js b/rn/app/src/components/menu/EmptyMenu.js
new file mode 100644
index 0000000..99763ac
--- /dev/null
+++ b/rn/app/src/components/menu/EmptyMenu.js
@@ -0,0 +1,32 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент пустого состояния меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View } = require('react-native'); //Базовые компоненты
+const AppText = require('../common/AppText'); //Общий текстовый компонент
+const styles = require('../../styles/menu/EmptyMenu.styles'); //Стили пустого меню
+
+//-----------
+//Тело модуля
+//-----------
+
+//Пустое состояние меню
+function EmptyMenu({ message = 'Нет доступных пунктов меню' }) {
+ return (
+
+ {message}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = EmptyMenu;
diff --git a/rn/app/src/components/menu/MenuDivider.js b/rn/app/src/components/menu/MenuDivider.js
new file mode 100644
index 0000000..c396c8a
--- /dev/null
+++ b/rn/app/src/components/menu/MenuDivider.js
@@ -0,0 +1,27 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент разделителя в меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View } = require('react-native'); //Базовые компоненты
+const styles = require('../../styles/menu/MenuDivider.styles'); //Стили разделителя
+
+//-----------
+//Тело модуля
+//-----------
+
+//Разделитель в меню
+function MenuDivider() {
+ return ;
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = MenuDivider;
diff --git a/rn/app/src/components/menu/MenuHeader.js b/rn/app/src/components/menu/MenuHeader.js
new file mode 100644
index 0000000..bb093d5
--- /dev/null
+++ b/rn/app/src/components/menu/MenuHeader.js
@@ -0,0 +1,52 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент заголовка меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View, Pressable } = require('react-native'); //Базовые компоненты
+const AppText = require('../common/AppText'); //Общий текстовый компонент
+const styles = require('../../styles/menu/MenuHeader.styles'); //Стили заголовка меню
+
+//-----------
+//Тело модуля
+//-----------
+
+//Заголовок меню
+function MenuHeader({ title, onClose, showCloseButton = true, style }) {
+ const handleClose = React.useCallback(() => {
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ }, [onClose]);
+
+ return (
+
+
+ {title || 'Меню'}
+
+ {showCloseButton ? (
+ [styles.closeButton, pressed && styles.closeButtonPressed]}
+ >
+
+ ×
+
+
+ ) : null}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = MenuHeader;
diff --git a/rn/app/src/components/menu/MenuItem.js b/rn/app/src/components/menu/MenuItem.js
new file mode 100644
index 0000000..b92c4f9
--- /dev/null
+++ b/rn/app/src/components/menu/MenuItem.js
@@ -0,0 +1,56 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент элемента меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { View, Pressable } = require('react-native'); //Базовые компоненты
+const AppText = require('../common/AppText'); //Общий текстовый компонент
+const styles = require('../../styles/menu/MenuItem.styles'); //Стили элемента меню
+
+//-----------
+//Тело модуля
+//-----------
+
+//Элемент меню
+function MenuItem({ title, icon, onPress, isDestructive = false, disabled = false, style, textStyle }) {
+ const handlePress = React.useCallback(() => {
+ if (!disabled && typeof onPress === 'function') {
+ onPress();
+ }
+ }, [disabled, onPress]);
+
+ return (
+ [
+ styles.menuItem,
+ pressed && !disabled && styles.menuItemPressed,
+ isDestructive && styles.menuItemDestructive,
+ disabled && styles.menuItemDisabled,
+ style
+ ]}
+ onPress={handlePress}
+ disabled={disabled}
+ >
+
+ {icon ? {icon} : null}
+
+ {title}
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = MenuItem;
diff --git a/rn/app/src/components/menu/MenuList.js b/rn/app/src/components/menu/MenuList.js
new file mode 100644
index 0000000..9d75876
--- /dev/null
+++ b/rn/app/src/components/menu/MenuList.js
@@ -0,0 +1,63 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент списка элементов меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { ScrollView, View } = require('react-native'); //Базовые компоненты
+const MenuItem = require('./MenuItem'); //Элемент меню
+const EmptyMenu = require('./EmptyMenu'); //Пустое меню
+const MenuDivider = require('./MenuDivider'); //Разделитель меню
+const styles = require('../../styles/menu/MenuList.styles'); //Стили списка меню
+
+//-----------
+//Тело модуля
+//-----------
+
+//Список элементов меню
+function MenuList({ items = [], onClose, style }) {
+ const handleItemPress = React.useCallback(
+ item => {
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ if (typeof item.onPress === 'function') {
+ item.onPress();
+ }
+ },
+ [onClose]
+ );
+
+ if (!Array.isArray(items) || items.length === 0) {
+ return ;
+ }
+
+ return (
+
+ {items.map((item, index) => (
+
+
+ ))}
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = MenuList;
diff --git a/rn/app/src/components/menu/SideMenu.js b/rn/app/src/components/menu/SideMenu.js
new file mode 100644
index 0000000..6fa9330
--- /dev/null
+++ b/rn/app/src/components/menu/SideMenu.js
@@ -0,0 +1,162 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Компонент бокового меню приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React
+const { Modal, View, Animated, Pressable } = require('react-native'); //Базовые компоненты
+const { useSafeAreaInsets } = require('react-native-safe-area-context'); //Отступы безопасной области
+const MenuHeader = require('./MenuHeader'); //Заголовок меню
+const MenuList = require('./MenuList'); //Список элементов меню
+const { widthPercentage, isTablet } = require('../../utils/responsive'); //Адаптивные утилиты
+const styles = require('../../styles/menu/SideMenu.styles'); //Стили бокового меню
+
+//-----------
+//Тело модуля
+//-----------
+
+//Ширина меню в зависимости от типа устройства
+const MENU_WIDTH_PHONE_PERCENT = 70;
+const MENU_WIDTH_TABLET_PERCENT = 40;
+const MENU_MAX_WIDTH = 360;
+const MENU_MIN_WIDTH = 280;
+
+//Длительность анимации (мс)
+const ANIMATION_DURATION_OPEN = 250;
+const ANIMATION_DURATION_CLOSE = 200;
+
+//Расчёт ширины меню с учётом адаптивности
+const calculateMenuWidth = () => {
+ const percent = isTablet() ? MENU_WIDTH_TABLET_PERCENT : MENU_WIDTH_PHONE_PERCENT;
+ const percentWidth = widthPercentage(percent);
+ return Math.max(MENU_MIN_WIDTH, Math.min(percentWidth, MENU_MAX_WIDTH));
+};
+
+//Боковое меню приложения
+function SideMenu({ visible, onClose, items = [], title = 'Меню', headerStyle, containerStyle, contentStyle }) {
+ //Получаем отступы безопасной области
+ const insets = useSafeAreaInsets();
+
+ //Вычисляем ширину меню
+ const menuWidth = calculateMenuWidth();
+
+ //Анимированные значения
+ const translateX = React.useRef(new Animated.Value(menuWidth)).current;
+ const backdropOpacity = React.useRef(new Animated.Value(0)).current;
+
+ //Флаг видимости модального окна
+ const [modalVisible, setModalVisible] = React.useState(false);
+
+ //Стили с учётом safe area
+ const safeAreaStyle = React.useMemo(
+ () => ({
+ paddingTop: insets.top,
+ paddingBottom: insets.bottom,
+ paddingRight: insets.right
+ }),
+ [insets.top, insets.bottom, insets.right]
+ );
+
+ //Обработка открытия меню
+ const openMenu = React.useCallback(() => {
+ setModalVisible(true);
+ translateX.setValue(menuWidth);
+ backdropOpacity.setValue(0);
+
+ Animated.parallel([
+ Animated.timing(translateX, {
+ toValue: 0,
+ duration: ANIMATION_DURATION_OPEN,
+ useNativeDriver: true
+ }),
+ Animated.timing(backdropOpacity, {
+ toValue: 1,
+ duration: ANIMATION_DURATION_OPEN,
+ useNativeDriver: true
+ })
+ ]).start();
+ }, [translateX, backdropOpacity, menuWidth]);
+
+ //Обработка закрытия меню
+ const closeMenu = React.useCallback(() => {
+ Animated.parallel([
+ Animated.timing(translateX, {
+ toValue: menuWidth,
+ duration: ANIMATION_DURATION_CLOSE,
+ useNativeDriver: true
+ }),
+ Animated.timing(backdropOpacity, {
+ toValue: 0,
+ duration: ANIMATION_DURATION_CLOSE,
+ useNativeDriver: true
+ })
+ ]).start(() => {
+ setModalVisible(false);
+ });
+ }, [translateX, backdropOpacity, menuWidth]);
+
+ //Обработчик нажатия на задний фон
+ const handleBackdropPress = React.useCallback(() => {
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ }, [onClose]);
+
+ //Обработчик аппаратной кнопки "Назад" (Android)
+ const handleRequestClose = React.useCallback(() => {
+ if (typeof onClose === 'function') {
+ onClose();
+ }
+ }, [onClose]);
+
+ //Отслеживание изменения видимости
+ React.useEffect(() => {
+ if (visible) {
+ openMenu();
+ } else if (modalVisible) {
+ closeMenu();
+ }
+ }, [visible, modalVisible, openMenu, closeMenu]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SideMenu;
diff --git a/rn/app/src/config/appConfig.js b/rn/app/src/config/appConfig.js
new file mode 100644
index 0000000..183d500
--- /dev/null
+++ b/rn/app/src/config/appConfig.js
@@ -0,0 +1,96 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Конфигурация приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { responsiveSize, isTablet } = require('../utils/responsive'); //Адаптивные утилиты
+const { Platform } = require('react-native');
+
+//---------
+//Константы
+//---------
+
+//Настройки сервера приложений
+const SYSTEM = {
+ //Адрес сервера приложений
+ SERVER: '',
+
+ //Таймаут сетевых запросов (мс)
+ REQUEST_TIMEOUT: 30000,
+
+ //Минимальная версия Android для работы с SQLite
+ MIN_ANDROID_VERSION: 7.0,
+
+ //Минимальная версия iOS для работы с SQLite
+ MIN_IOS_VERSION: 11.0
+};
+
+//Настройки локального хранилища
+const LOCAL_DB = {
+ //Ключ для хранения данных предрейсовых осмотров
+ INSPECTIONS_KEY: 'pretrip_inspections',
+
+ //Резервное хранилище для старых устройств (AsyncStorage)
+ FALLBACK_STORAGE_KEY: 'pretrip_fallback_storage'
+};
+
+//Настройки интерфейса
+const UI = {
+ //Отступы по умолчанию (адаптивные)
+ PADDING: responsiveSize(isTablet() ? 24 : 16),
+
+ //Радиус скругления по умолчанию (адаптивный)
+ BORDER_RADIUS: responsiveSize(isTablet() ? 12 : 8),
+
+ //Размеры шрифтов
+ FONT_SIZE_XS: responsiveSize(12),
+ FONT_SIZE_SM: responsiveSize(14),
+ FONT_SIZE_MD: responsiveSize(16),
+ FONT_SIZE_LG: responsiveSize(18),
+ FONT_SIZE_XL: responsiveSize(20),
+ FONT_SIZE_2XL: responsiveSize(24),
+
+ //Высоты элементов
+ BUTTON_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
+ INPUT_HEIGHT: responsiveSize(Platform.OS === 'ios' ? 48 : 44),
+ HEADER_HEIGHT: responsiveSize(isTablet() ? 80 : Platform.OS === 'ios' ? 70 : 56)
+};
+
+//Проверка совместимости устройства
+const COMPATIBILITY = {
+ //Проверяем версию Android
+ isAndroidCompatible: () => {
+ if (Platform.OS !== 'android') return true;
+
+ const majorVersion = parseInt(Platform.Version, 10);
+ return majorVersion >= 24; // Android 7.0 = API 24
+ },
+
+ //Проверяем версию iOS
+ isIOSCompatible: () => {
+ if (Platform.OS !== 'ios') return true;
+
+ const majorVersion = parseInt(Platform.Version, 10);
+ return majorVersion >= 11; // iOS 11.0
+ },
+
+ //Общая проверка совместимости
+ isDeviceCompatible: () => {
+ return COMPATIBILITY.isAndroidCompatible() && COMPATIBILITY.isIOSCompatible();
+ }
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ SYSTEM,
+ LOCAL_DB,
+ UI,
+ COMPATIBILITY
+};
diff --git a/rn/app/src/config/theme.js b/rn/app/src/config/theme.js
new file mode 100644
index 0000000..911bdbe
--- /dev/null
+++ b/rn/app/src/config/theme.js
@@ -0,0 +1,59 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Единая цветовая схема (тема приложения)
+*/
+
+//---------
+//Константы
+//---------
+
+//Базовая палитра приложения:
+//- основной фон и элементы интерфейса - белый;
+//- акцентный цвет - голубой.
+const APP_COLORS = {
+ //Базовые
+ white: '#FFFFFF',
+ black: '#000000',
+
+ //Основной голубой акцент
+ primary: '#2563EB',
+ primaryDark: '#1D4ED8',
+ primaryLight: '#60A5FA',
+ primaryExtraLight: '#DBEAFE',
+
+ //Фоны
+ background: '#F8FAFC',
+ surface: '#FFFFFF',
+ surfaceAlt: '#F1F5F9',
+
+ //Текст
+ textPrimary: '#1E293B',
+ textSecondary: '#64748B',
+ textTertiary: '#94A3B8',
+ textInverse: '#FFFFFF',
+
+ //Состояния
+ error: '#DC2626',
+ errorLight: '#FEE2E2',
+ warning: '#F59E0B',
+ warningLight: '#FEF3C7',
+ success: '#16A34A',
+ successLight: '#DCFCE7',
+
+ //Дополнительные
+ borderSubtle: '#E2E8F0',
+ borderMedium: '#CBD5E1',
+ overlay: 'rgba(15, 23, 42, 0.5)',
+
+ //Семантические
+ info: '#3B82F6',
+ infoLight: '#DBEAFE'
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ APP_COLORS
+};
diff --git a/rn/app/src/database/SQLiteDatabase.js b/rn/app/src/database/SQLiteDatabase.js
new file mode 100644
index 0000000..5567df3
--- /dev/null
+++ b/rn/app/src/database/SQLiteDatabase.js
@@ -0,0 +1,351 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Модуль для работы с SQLite базой данных
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { open } = require('react-native-quick-sqlite');
+
+//Импорт утилиты для загрузки SQL файлов
+const SQLFileLoader = require('./sql/SQLFileLoader');
+
+//---------
+//Константы
+//---------
+
+const DB_NAME = 'pretrip_inspections.db';
+
+//-----------
+//Тело модуля
+//-----------
+
+class SQLiteDatabase {
+ constructor() {
+ this.db = null;
+ this.isInitialized = false;
+ this.sqlQueries = null;
+ }
+
+ //Инициализация базы данных
+ async initialize() {
+ if (this.isInitialized) {
+ return this.db;
+ }
+
+ try {
+ //Загружаем SQL файлы (синхронно)
+ this.sqlQueries = SQLFileLoader.loadAllSQLFiles();
+
+ //Открываем базу данных
+ this.db = open({
+ name: DB_NAME,
+ location: 'default'
+ });
+
+ console.log('База данных успешно открыта');
+
+ //Настраиваем базу данных
+ await this.setupDatabase();
+
+ this.isInitialized = true;
+ return this.db;
+ } catch (error) {
+ console.error('Ошибка инициализации базы данных:', error);
+ throw error;
+ }
+ }
+
+ //Настройка базы данных (создание таблиц и индексов)
+ async setupDatabase() {
+ if (!this.db || !this.sqlQueries) {
+ throw new Error('База данных или SQL запросы не инициализированы');
+ }
+
+ try {
+ //Выполняем SQL запросы последовательно
+ await this.executeQuery(this.sqlQueries.CREATE_TABLE_APP_SETTINGS);
+ console.log('Таблица app_settings создана/проверена');
+
+ await this.executeQuery(this.sqlQueries.CREATE_TABLE_INSPECTIONS);
+ console.log('Таблица inspections создана/проверена');
+
+ await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS);
+ console.log('Индекс idx_inspections_status создан/проверен');
+
+ await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_CREATED);
+ console.log('Индекс idx_inspections_created создан/проверен');
+
+ console.log('Все таблицы и индексы созданы/проверены');
+ } catch (error) {
+ console.error('Ошибка настройки базы данных:', error);
+ throw error;
+ }
+ }
+
+ //Выполнение SQL запроса
+ async executeQuery(sql, params = []) {
+ if (!this.db) {
+ throw new Error('База данных не инициализирована');
+ }
+
+ try {
+ const result = await this.db.executeAsync(sql, params);
+ return result;
+ } catch (error) {
+ console.error('Ошибка выполнения SQL запроса:', error, 'SQL:', sql, 'Params:', params);
+ throw error;
+ }
+ }
+
+ //Получение настройки
+ async getSetting(key) {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.SETTINGS_GET, [key]);
+
+ if (result.rows && result.rows.length > 0) {
+ return result.rows.item(0).value;
+ }
+ return null;
+ } catch (error) {
+ console.error('Ошибка получения настройки:', error);
+ throw error;
+ }
+ }
+
+ //Сохранение настройки
+ async setSetting(key, value) {
+ try {
+ const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
+ await this.executeQuery(this.sqlQueries.SETTINGS_SET, [key, stringValue]);
+ return true;
+ } catch (error) {
+ console.error('Ошибка сохранения настройки:', error);
+ throw error;
+ }
+ }
+
+ //Удаление настройки
+ async deleteSetting(key) {
+ try {
+ await this.executeQuery(this.sqlQueries.SETTINGS_DELETE, [key]);
+ return true;
+ } catch (error) {
+ console.error('Ошибка удаления настройки:', error);
+ throw error;
+ }
+ }
+
+ //Получение всех настроек
+ async getAllSettings() {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.SETTINGS_GET_ALL, []);
+ const settings = {};
+
+ if (result.rows && result.rows.length > 0) {
+ for (let i = 0; i < result.rows.length; i++) {
+ const row = result.rows.item(i);
+ try {
+ settings[row.key] = JSON.parse(row.value);
+ } catch {
+ settings[row.key] = row.value;
+ }
+ }
+ }
+
+ return settings;
+ } catch (error) {
+ console.error('Ошибка получения всех настроек:', error);
+ throw error;
+ }
+ }
+
+ //Очистка всех настроек
+ async clearSettings() {
+ try {
+ await this.executeQuery(this.sqlQueries.SETTINGS_CLEAR_ALL, []);
+ return true;
+ } catch (error) {
+ console.error('Ошибка очистки настроек:', error);
+ throw error;
+ }
+ }
+
+ //Сохранение осмотра
+ async saveInspection(inspection) {
+ try {
+ const { id, title, status, data } = inspection;
+ const dataString = data ? JSON.stringify(data) : null;
+
+ await this.executeQuery(this.sqlQueries.INSPECTIONS_UPSERT, [id, title, status, id, dataString]);
+ return true;
+ } catch (error) {
+ console.error('Ошибка сохранения осмотра:', error);
+ throw error;
+ }
+ }
+
+ //Получение всех осмотров
+ async getInspections() {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_ALL, []);
+ const inspections = [];
+
+ if (result.rows && result.rows.length > 0) {
+ for (let i = 0; i < result.rows.length; i++) {
+ const row = result.rows.item(i);
+ const inspection = {
+ id: row.id,
+ title: row.title,
+ status: row.status,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at
+ };
+
+ if (row.data) {
+ try {
+ inspection.data = JSON.parse(row.data);
+ } catch {
+ inspection.data = row.data;
+ }
+ }
+
+ inspections.push(inspection);
+ }
+ }
+
+ return inspections;
+ } catch (error) {
+ console.error('Ошибка получения осмотров:', error);
+ throw error;
+ }
+ }
+
+ //Получение осмотра по ID
+ async getInspectionById(id) {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_GET_BY_ID, [id]);
+
+ if (result.rows && result.rows.length > 0) {
+ const row = result.rows.item(0);
+ const inspection = {
+ id: row.id,
+ title: row.title,
+ status: row.status,
+ createdAt: row.created_at,
+ updatedAt: row.updated_at
+ };
+
+ if (row.data) {
+ try {
+ inspection.data = JSON.parse(row.data);
+ } catch {
+ inspection.data = row.data;
+ }
+ }
+
+ return inspection;
+ }
+ return null;
+ } catch (error) {
+ console.error('Ошибка получения осмотра по ID:', error);
+ throw error;
+ }
+ }
+
+ //Удаление осмотра
+ async deleteInspection(id) {
+ try {
+ await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE, [id]);
+ return true;
+ } catch (error) {
+ console.error('Ошибка удаления осмотра:', error);
+ throw error;
+ }
+ }
+
+ //Удаление всех осмотров
+ async clearInspections() {
+ try {
+ await this.executeQuery(this.sqlQueries.INSPECTIONS_DELETE_ALL, []);
+ return true;
+ } catch (error) {
+ console.error('Ошибка очистки осмотров:', error);
+ throw error;
+ }
+ }
+
+ //Получение количества осмотров
+ async getInspectionsCount() {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.INSPECTIONS_COUNT, []);
+
+ if (result.rows && result.rows.length > 0) {
+ return result.rows.item(0).count;
+ }
+ return 0;
+ } catch (error) {
+ console.error('Ошибка получения количества осмотров:', error);
+ throw error;
+ }
+ }
+
+ //Оптимизация базы данных
+ async vacuum() {
+ try {
+ await this.executeQuery(this.sqlQueries.UTILITY_VACUUM, []);
+ return true;
+ } catch (error) {
+ console.error('Ошибка оптимизации базы данных:', error);
+ throw error;
+ }
+ }
+
+ //Проверка существования таблицы
+ async checkTableExists(tableName) {
+ try {
+ const result = await this.executeQuery(this.sqlQueries.UTILITY_CHECK_TABLE, [tableName]);
+ return result.rows && result.rows.length > 0 && result.rows.item(0).exists === 1;
+ } catch (error) {
+ console.error('Ошибка проверки существования таблицы:', error);
+ throw error;
+ }
+ }
+
+ //Удаление таблицы
+ async dropTable(tableName) {
+ try {
+ await this.executeQuery(this.sqlQueries.UTILITY_DROP_TABLE, [tableName]);
+ return true;
+ } catch (error) {
+ console.error('Ошибка удаления таблицы:', error);
+ throw error;
+ }
+ }
+
+ //Закрытие базы данных
+ async close() {
+ try {
+ if (this.db) {
+ // В react-native-quick-sqlite нет явного метода close
+ // База данных закрывается автоматически при уничтожении объекта
+ this.db = null;
+ this.isInitialized = false;
+ console.log('База данных закрыта');
+ }
+ } catch (error) {
+ console.error('Ошибка закрытия базы данных:', error);
+ this.db = null;
+ this.isInitialized = false;
+ throw error;
+ }
+ }
+}
+
+//-----------------
+//Интерфейс модуля
+//-----------------
+
+module.exports = new SQLiteDatabase();
diff --git a/rn/app/src/database/sql/SQLFileLoader.js b/rn/app/src/database/sql/SQLFileLoader.js
new file mode 100644
index 0000000..f71523d
--- /dev/null
+++ b/rn/app/src/database/sql/SQLFileLoader.js
@@ -0,0 +1,27 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Утилита для загрузки SQL файлов
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const SQLQueries = require('./SQLQueries');
+
+//------------
+//Тело модуля
+//------------
+
+class SQLFileLoader {
+ //Загрузка всех SQL файлов (синхронно)
+ static loadAllSQLFiles() {
+ return SQLQueries;
+ }
+}
+
+//-----------------
+//Интерфейс модуля
+//-----------------
+
+module.exports = SQLFileLoader;
diff --git a/rn/app/src/database/sql/SQLQueries.js b/rn/app/src/database/sql/SQLQueries.js
new file mode 100644
index 0000000..0cf5d8a
--- /dev/null
+++ b/rn/app/src/database/sql/SQLQueries.js
@@ -0,0 +1,70 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Индексный файл для всех 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_INDEX_INSPECTIONS_STATUS = require('./inspections/create_index_inspections_status.sql');
+const CREATE_INDEX_INSPECTIONS_CREATED = require('./inspections/create_index_inspections_created.sql');
+
+//Настройки
+const SETTINGS_GET = require('./settings/get_setting.sql');
+const SETTINGS_SET = require('./settings/set_setting.sql');
+const SETTINGS_DELETE = require('./settings/delete_setting.sql');
+const SETTINGS_CLEAR_ALL = require('./settings/clear_all_settings.sql');
+const SETTINGS_GET_ALL = require('./settings/get_all_settings.sql');
+
+//Осмотры
+const INSPECTIONS_INSERT = require('./inspections/insert_inspection.sql');
+const INSPECTIONS_UPSERT = require('./inspections/upsert_inspection.sql');
+const INSPECTIONS_GET_ALL = require('./inspections/get_all_inspections.sql');
+const INSPECTIONS_GET_BY_ID = require('./inspections/get_inspection_by_id.sql');
+const INSPECTIONS_DELETE = require('./inspections/delete_inspection.sql');
+const INSPECTIONS_DELETE_ALL = require('./inspections/delete_all_inspections.sql');
+const INSPECTIONS_COUNT = require('./inspections/count_inspections.sql');
+
+//Утилиты
+const UTILITY_CHECK_TABLE = require('./utility/check_table_exists.sql');
+const UTILITY_DROP_TABLE = require('./utility/drop_table.sql');
+const UTILITY_VACUUM = require('./utility/vacuum.sql');
+
+//-----------
+//Тело модуля
+//-----------
+
+//Сбор всех SQL запросов в один объект
+const SQLQueries = {
+ CREATE_TABLE_APP_SETTINGS,
+ CREATE_TABLE_INSPECTIONS,
+ CREATE_INDEX_INSPECTIONS_STATUS,
+ CREATE_INDEX_INSPECTIONS_CREATED,
+ SETTINGS_GET,
+ SETTINGS_SET,
+ SETTINGS_DELETE,
+ SETTINGS_CLEAR_ALL,
+ SETTINGS_GET_ALL,
+ INSPECTIONS_INSERT,
+ INSPECTIONS_UPSERT,
+ INSPECTIONS_GET_ALL,
+ INSPECTIONS_GET_BY_ID,
+ INSPECTIONS_DELETE,
+ INSPECTIONS_DELETE_ALL,
+ INSPECTIONS_COUNT,
+ UTILITY_CHECK_TABLE,
+ UTILITY_DROP_TABLE,
+ UTILITY_VACUUM
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SQLQueries;
diff --git a/rn/app/src/database/sql/inspections/count_inspections.sql.js b/rn/app/src/database/sql/inspections/count_inspections.sql.js
new file mode 100644
index 0000000..652a75c
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/count_inspections.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: подсчет количества осмотров
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_COUNT = `
+-- Подсчет количества осмотров
+SELECT COUNT(*) as count FROM inspections;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_COUNT;
diff --git a/rn/app/src/database/sql/inspections/create_index_inspections_created.sql.js b/rn/app/src/database/sql/inspections/create_index_inspections_created.sql.js
new file mode 100644
index 0000000..8b5867b
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/create_index_inspections_created.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ 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;
diff --git a/rn/app/src/database/sql/inspections/create_index_inspections_status.sql.js b/rn/app/src/database/sql/inspections/create_index_inspections_status.sql.js
new file mode 100644
index 0000000..b0cfe5c
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/create_index_inspections_status.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: создание индекса по статусу
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const CREATE_INDEX_INSPECTIONS_STATUS = `
+-- Индекс для быстрого поиска осмотров по статусу
+CREATE INDEX IF NOT EXISTS idx_inspections_status ON inspections(status);
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = CREATE_INDEX_INSPECTIONS_STATUS;
diff --git a/rn/app/src/database/sql/inspections/create_table_inspections.sql.js b/rn/app/src/database/sql/inspections/create_table_inspections.sql.js
new file mode 100644
index 0000000..84fa51c
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/create_table_inspections.sql.js
@@ -0,0 +1,26 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ 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;
\ No newline at end of file
diff --git a/rn/app/src/database/sql/inspections/delete_all_inspections.sql.js b/rn/app/src/database/sql/inspections/delete_all_inspections.sql.js
new file mode 100644
index 0000000..f1fa07d
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/delete_all_inspections.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: удаление всех осмотров
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_DELETE_ALL = `
+-- Удаление всех осмотров
+DELETE FROM inspections;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_DELETE_ALL;
diff --git a/rn/app/src/database/sql/inspections/delete_inspection.sql.js b/rn/app/src/database/sql/inspections/delete_inspection.sql.js
new file mode 100644
index 0000000..cbe26ec
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/delete_inspection.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: удаление осмотра
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_DELETE = `
+-- Удаление осмотра по ID
+DELETE FROM inspections WHERE id = ?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_DELETE;
diff --git a/rn/app/src/database/sql/inspections/get_all_inspections.sql.js b/rn/app/src/database/sql/inspections/get_all_inspections.sql.js
new file mode 100644
index 0000000..ee1cdc8
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/get_all_inspections.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: получение всех осмотров
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_GET_ALL = `
+-- Получение всех осмотров
+SELECT * FROM inspections ORDER BY created_at DESC;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_GET_ALL;
diff --git a/rn/app/src/database/sql/inspections/get_inspection_by_id.sql.js b/rn/app/src/database/sql/inspections/get_inspection_by_id.sql.js
new file mode 100644
index 0000000..7c6748e
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/get_inspection_by_id.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: получение осмотра по ID
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_GET_BY_ID = `
+-- Получение осмотра по ID
+SELECT * FROM inspections WHERE id = ?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_GET_BY_ID;
diff --git a/rn/app/src/database/sql/inspections/insert_inspection.sql.js b/rn/app/src/database/sql/inspections/insert_inspection.sql.js
new file mode 100644
index 0000000..58fe4dd
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/insert_inspection.sql.js
@@ -0,0 +1,20 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: вставка нового осмотра
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const INSPECTIONS_INSERT = `
+-- Вставка нового осмотра
+INSERT INTO inspections (id, title, status, created_at, data)
+VALUES (?, ?, ?, ?, ?);
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = INSPECTIONS_INSERT;
diff --git a/rn/app/src/database/sql/inspections/upsert_inspection.sql.js b/rn/app/src/database/sql/inspections/upsert_inspection.sql.js
new file mode 100644
index 0000000..e08a019
--- /dev/null
+++ b/rn/app/src/database/sql/inspections/upsert_inspection.sql.js
@@ -0,0 +1,20 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ 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;
diff --git a/rn/app/src/database/sql/settings/clear_all_settings.sql.js b/rn/app/src/database/sql/settings/clear_all_settings.sql.js
new file mode 100644
index 0000000..3ff8556
--- /dev/null
+++ b/rn/app/src/database/sql/settings/clear_all_settings.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: удаление всех настроек
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const SETTINGS_CLEAR_ALL = `
+-- Удаление всех настроек
+DELETE FROM app_settings;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SETTINGS_CLEAR_ALL;
diff --git a/rn/app/src/database/sql/settings/create_table_app_settings.sql.js b/rn/app/src/database/sql/settings/create_table_app_settings.sql.js
new file mode 100644
index 0000000..7792ae0
--- /dev/null
+++ b/rn/app/src/database/sql/settings/create_table_app_settings.sql.js
@@ -0,0 +1,23 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: создание таблицы настроек приложения
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const CREATE_TABLE_APP_SETTINGS = `
+-- Таблица для хранения настроек приложения
+CREATE TABLE IF NOT EXISTS app_settings (
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = CREATE_TABLE_APP_SETTINGS;
diff --git a/rn/app/src/database/sql/settings/delete_setting.sql.js b/rn/app/src/database/sql/settings/delete_setting.sql.js
new file mode 100644
index 0000000..9a1c87b
--- /dev/null
+++ b/rn/app/src/database/sql/settings/delete_setting.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: удаление настройки
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const SETTINGS_DELETE = `
+-- Удаление настройки по ключу
+DELETE FROM app_settings WHERE key = ?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SETTINGS_DELETE;
diff --git a/rn/app/src/database/sql/settings/get_all_settings.sql.js b/rn/app/src/database/sql/settings/get_all_settings.sql.js
new file mode 100644
index 0000000..2969ca4
--- /dev/null
+++ b/rn/app/src/database/sql/settings/get_all_settings.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: получение всех настроек
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const SETTINGS_GET_ALL = `
+-- Получение всех настроек
+SELECT key, value FROM app_settings;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SETTINGS_GET_ALL;
diff --git a/rn/app/src/database/sql/settings/get_setting.sql.js b/rn/app/src/database/sql/settings/get_setting.sql.js
new file mode 100644
index 0000000..d1898f8
--- /dev/null
+++ b/rn/app/src/database/sql/settings/get_setting.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: получение настройки по ключу
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const SETTINGS_GET = `
+-- Получение значения настройки по ключу
+SELECT value FROM app_settings WHERE key = ?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SETTINGS_GET;
diff --git a/rn/app/src/database/sql/settings/set_setting.sql.js b/rn/app/src/database/sql/settings/set_setting.sql.js
new file mode 100644
index 0000000..6a4c4a4
--- /dev/null
+++ b/rn/app/src/database/sql/settings/set_setting.sql.js
@@ -0,0 +1,20 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: сохранение или обновление настройки
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const SETTINGS_SET = `
+-- Сохранение или обновление настройки
+INSERT OR REPLACE INTO app_settings (key, value, updated_at)
+VALUES (?, ?, CURRENT_TIMESTAMP);
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SETTINGS_SET;
diff --git a/rn/app/src/database/sql/utility/check_table_exists.sql.js b/rn/app/src/database/sql/utility/check_table_exists.sql.js
new file mode 100644
index 0000000..ae837cb
--- /dev/null
+++ b/rn/app/src/database/sql/utility/check_table_exists.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: проверка существования таблицы
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const UTILITY_CHECK_TABLE = `
+-- Проверка существования таблицы
+SELECT 1 as exists FROM sqlite_master WHERE type='table' AND name=?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = UTILITY_CHECK_TABLE;
diff --git a/rn/app/src/database/sql/utility/drop_table.sql.js b/rn/app/src/database/sql/utility/drop_table.sql.js
new file mode 100644
index 0000000..4a1ccea
--- /dev/null
+++ b/rn/app/src/database/sql/utility/drop_table.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: удаление таблицы
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const UTILITY_DROP_TABLE = `
+-- Удаление таблицы
+DROP TABLE IF EXISTS ?;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = UTILITY_DROP_TABLE;
diff --git a/rn/app/src/database/sql/utility/vacuum.sql.js b/rn/app/src/database/sql/utility/vacuum.sql.js
new file mode 100644
index 0000000..ee644c8
--- /dev/null
+++ b/rn/app/src/database/sql/utility/vacuum.sql.js
@@ -0,0 +1,19 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ SQL запрос: оптимизация базы данных
+*/
+
+//-----------
+//Тело модуля
+//-----------
+
+const UTILITY_VACUUM = `
+-- Оптимизация базы данных
+VACUUM;
+`;
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = UTILITY_VACUUM;
diff --git a/rn/app/src/hooks/useAppMessaging.js b/rn/app/src/hooks/useAppMessaging.js
new file mode 100644
index 0000000..850bfcd
--- /dev/null
+++ b/rn/app/src/hooks/useAppMessaging.js
@@ -0,0 +1,153 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук сообщений приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { APP_MESSAGE_VARIANT } = require('../components/common/AppMessage'); //Константы сообщений
+
+//---------
+//Константы
+//---------
+
+//Начальное состояние сообщений
+const INITIAL_STATE = {
+ visible: false,
+ variant: APP_MESSAGE_VARIANT.INFO,
+ title: '',
+ message: '',
+ buttons: null,
+ containerStyle: null,
+ contentStyle: null,
+ titleStyle: null,
+ messageStyle: null,
+ headerStyle: null
+};
+
+//Типы сообщений
+const MSG_TYPE = {
+ INFO: APP_MESSAGE_VARIANT.INFO,
+ WARN: APP_MESSAGE_VARIANT.WARN,
+ ERR: APP_MESSAGE_VARIANT.ERR,
+ SUCCESS: APP_MESSAGE_VARIANT.SUCCESS
+};
+
+//Типы действий
+const MSG_AT = {
+ SHOW_MSG: 'SHOW_MSG',
+ HIDE_MSG: 'HIDE_MSG'
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Редьюсер состояния сообщений
+const messagingReducer = (state, action) => {
+ switch (action.type) {
+ case MSG_AT.SHOW_MSG:
+ return {
+ ...state,
+ visible: true,
+ variant: action.payload.variant || MSG_TYPE.INFO,
+ title: action.payload.title || '',
+ message: action.payload.message || '',
+ buttons: action.payload.buttons || null,
+ containerStyle: action.payload.containerStyle || null,
+ contentStyle: action.payload.contentStyle || null,
+ titleStyle: action.payload.titleStyle || null,
+ messageStyle: action.payload.messageStyle || null,
+ headerStyle: action.payload.headerStyle || null
+ };
+ case MSG_AT.HIDE_MSG:
+ return {
+ ...state,
+ visible: false
+ };
+ default:
+ return state;
+ }
+};
+
+//Хук сообщений приложения
+function useAppMessaging() {
+ const [state, dispatch] = React.useReducer(messagingReducer, INITIAL_STATE);
+
+ //Базовое отображение сообщения
+ const showMessage = React.useCallback((payload = {}) => {
+ dispatch({
+ type: MSG_AT.SHOW_MSG,
+ payload
+ });
+ }, []);
+
+ //Сообщение - ошибка
+ const showError = React.useCallback(
+ (message, options = {}) =>
+ showMessage({
+ ...options,
+ variant: MSG_TYPE.ERR,
+ message
+ }),
+ [showMessage]
+ );
+
+ //Сообщение - информация
+ const showInfo = React.useCallback(
+ (message, options = {}) =>
+ showMessage({
+ ...options,
+ variant: MSG_TYPE.INFO,
+ message
+ }),
+ [showMessage]
+ );
+
+ //Сообщение - предупреждение
+ const showWarn = React.useCallback(
+ (message, options = {}) =>
+ showMessage({
+ ...options,
+ variant: MSG_TYPE.WARN,
+ message
+ }),
+ [showMessage]
+ );
+
+ //Сообщение - успех
+ const showSuccess = React.useCallback(
+ (message, options = {}) =>
+ showMessage({
+ ...options,
+ variant: MSG_TYPE.SUCCESS,
+ message
+ }),
+ [showMessage]
+ );
+
+ //Сокрытие сообщения
+ const hideMessage = React.useCallback(() => {
+ dispatch({ type: MSG_AT.HIDE_MSG });
+ }, []);
+
+ return {
+ MSG_TYPE,
+ state,
+ showMessage,
+ showError,
+ showInfo,
+ showWarn,
+ showSuccess,
+ hideMessage
+ };
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useAppMessaging;
diff --git a/rn/app/src/hooks/useAppMode.js b/rn/app/src/hooks/useAppMode.js
new file mode 100644
index 0000000..996455f
--- /dev/null
+++ b/rn/app/src/hooks/useAppMode.js
@@ -0,0 +1,112 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук режима работы приложения (ONLINE / OFFLINE / NOT_CONNECTED)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react');
+const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
+
+//---------
+//Константы
+//---------
+
+const APP_MODE = {
+ ONLINE: 'ONLINE',
+ OFFLINE: 'OFFLINE',
+ NOT_CONNECTED: 'NOT_CONNECTED'
+};
+
+const STORAGE_KEY = 'pretrip_app_mode';
+
+//-----------
+//Тело модуля
+//-----------
+
+//Хук режима работы приложения
+function useAppMode() {
+ const { getSetting, setSetting, isDbReady } = useAppLocalDbContext();
+ const [mode, setMode] = React.useState(APP_MODE.NOT_CONNECTED);
+ const [isInitialized, setIsInitialized] = React.useState(false);
+
+ //Предотвращение повторной загрузки
+ const loadingRef = React.useRef(false);
+
+ //Загрузка режима из базы данных при готовности БД
+ React.useEffect(() => {
+ //Выходим, если БД не готова или уже загружаем/загрузили
+ if (!isDbReady || loadingRef.current || isInitialized) {
+ return;
+ }
+
+ loadingRef.current = true;
+
+ const loadMode = async () => {
+ try {
+ const savedMode = await getSetting(STORAGE_KEY);
+ if (savedMode && Object.values(APP_MODE).includes(savedMode)) {
+ setMode(savedMode);
+ } else {
+ const serverUrl = await getSetting('app_server_url');
+ if (serverUrl) {
+ setMode(APP_MODE.ONLINE);
+ }
+ }
+ } catch (error) {
+ console.error('Ошибка загрузки режима:', error);
+ } finally {
+ setIsInitialized(true);
+ loadingRef.current = false;
+ }
+ };
+
+ loadMode();
+ }, [isDbReady, isInitialized, getSetting]);
+
+ //Сохранение режима в базу данных при изменении
+ React.useEffect(() => {
+ if (!isInitialized || !isDbReady) {
+ return;
+ }
+
+ const saveMode = async () => {
+ try {
+ await setSetting(STORAGE_KEY, mode);
+ } catch (error) {
+ console.error('Ошибка сохранения режима:', error);
+ }
+ };
+
+ saveMode();
+ }, [mode, isInitialized, isDbReady, setSetting]);
+
+ //Установка режима ONLINE
+ const setOnline = React.useCallback(() => setMode(APP_MODE.ONLINE), []);
+
+ //Установка режима OFFLINE
+ const setOffline = React.useCallback(() => setMode(APP_MODE.OFFLINE), []);
+
+ //Установка режима NOT_CONNECTED
+ const setNotConnected = React.useCallback(() => setMode(APP_MODE.NOT_CONNECTED), []);
+
+ return {
+ APP_MODE,
+ mode,
+ setMode,
+ setOnline,
+ setOffline,
+ setNotConnected,
+ isOnline: mode === APP_MODE.ONLINE,
+ isOffline: mode === APP_MODE.OFFLINE,
+ isNotConnected: mode === APP_MODE.NOT_CONNECTED
+ };
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useAppMode;
diff --git a/rn/app/src/hooks/useAppNavigation.js b/rn/app/src/hooks/useAppNavigation.js
new file mode 100644
index 0000000..9aac1b6
--- /dev/null
+++ b/rn/app/src/hooks/useAppNavigation.js
@@ -0,0 +1,86 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук навигации приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+
+//---------
+//Константы
+//---------
+
+//Экраны приложения
+const SCREENS = {
+ MAIN: 'MAIN',
+ SETTINGS: 'SETTINGS'
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Хук навигации приложения
+const useAppNavigation = () => {
+ const [navigationState, setNavigationState] = React.useState({
+ currentScreen: SCREENS.MAIN,
+ screenParams: {},
+ history: [SCREENS.MAIN]
+ });
+
+ //Навигация на экран
+ const navigate = React.useCallback((screen, params = {}) => {
+ setNavigationState(prev => ({
+ currentScreen: screen,
+ screenParams: params,
+ history: [...prev.history, screen]
+ }));
+ }, []);
+
+ //Возврат назад
+ const goBack = React.useCallback(() => {
+ setNavigationState(prev => {
+ if (prev.history.length <= 1) {
+ return prev;
+ }
+
+ const newHistory = [...prev.history];
+ newHistory.pop();
+ const previousScreen = newHistory[newHistory.length - 1];
+
+ return {
+ currentScreen: previousScreen,
+ screenParams: {},
+ history: newHistory
+ };
+ });
+ }, []);
+
+ //Сброс навигации
+ const reset = React.useCallback(() => {
+ setNavigationState({
+ currentScreen: SCREENS.MAIN,
+ screenParams: {},
+ history: [SCREENS.MAIN]
+ });
+ }, []);
+
+ return {
+ SCREENS,
+ currentScreen: navigationState.currentScreen,
+ screenParams: navigationState.screenParams,
+ navigate,
+ goBack,
+ reset,
+ canGoBack: navigationState.history.length > 1
+ };
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useAppNavigation;
diff --git a/rn/app/src/hooks/useAppServer.js b/rn/app/src/hooks/useAppServer.js
new file mode 100644
index 0000000..075885c
--- /dev/null
+++ b/rn/app/src/hooks/useAppServer.js
@@ -0,0 +1,146 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук для взаимодействия с сервером приложений
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react');
+const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
+
+//---------
+//Константы
+//---------
+
+const RESP_STATUS_OK = 'OK';
+const RESP_STATUS_ERR = 'ERR';
+
+const ERR_APPSERVER = 'Ошибка сервера приложений';
+const ERR_NETWORK = 'Ошибка соединения с сервером';
+const ERR_UNEXPECTED = 'Неожиданный ответ сервера';
+const ERR_ABORTED = 'Запрос прерван принудительно';
+
+//-----------------------
+//Вспомогательные функции
+//-----------------------
+
+//Создание объекта ошибки ответа
+const makeRespErr = ({ message }) => ({
+ status: RESP_STATUS_ERR,
+ message
+});
+
+//Проверка является ли ответ ошибкой
+const isRespErr = resp => resp && resp.status === RESP_STATUS_ERR;
+
+//Получение сообщения об ошибке из ответа
+const getRespErrMessage = resp => (isRespErr(resp) && resp.message ? resp.message : '');
+
+//-----------
+//Тело модуля
+//-----------
+
+//Хук для взаимодействия с сервером приложений
+function useAppServer() {
+ const { getSetting } = useAppLocalDbContext();
+ const abortControllerRef = React.useRef(null);
+
+ //Принудительная отмена активного запроса
+ const abort = React.useCallback(() => {
+ if (abortControllerRef.current) {
+ abortControllerRef.current.abort();
+ }
+ }, []);
+
+ //Выполнение произвольного действия на сервере
+ const executeAction = React.useCallback(
+ async ({ path = '', method = 'POST', payload = null } = {}) => {
+ let baseURL = '';
+ try {
+ baseURL = (await getSetting('app_server_url')) || '';
+ } catch (e) {
+ console.warn('Не удалось прочитать настройки сервера:', e);
+ }
+
+ if (!baseURL) {
+ return makeRespErr({
+ message: 'Адрес сервера не настроен. Пожалуйста, настройте сервер в настройках приложения.'
+ });
+ }
+
+ const normalizedBaseURL = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+ const url = `${normalizedBaseURL}${normalizedPath}`;
+
+ const abortController = new AbortController();
+ abortControllerRef.current = abortController;
+
+ let response = null;
+ let responseJSON = null;
+
+ try {
+ response = await fetch(url, {
+ method,
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: payload ? JSON.stringify(payload) : null,
+ signal: abortController.signal
+ });
+ } catch (e) {
+ if (abortController.signal.aborted === true) {
+ return makeRespErr({ message: ERR_ABORTED });
+ }
+ return makeRespErr({
+ message: `${ERR_NETWORK}: ${e.message || 'неопределённая ошибка'}`
+ });
+ } finally {
+ abortControllerRef.current = null;
+ }
+
+ if (!response.ok) {
+ return makeRespErr({
+ message: `${ERR_APPSERVER}: ${response.status} ${response.statusText || ''}`
+ });
+ }
+
+ try {
+ responseJSON = await response.json();
+ } catch (e) {
+ return makeRespErr({ message: ERR_UNEXPECTED });
+ }
+
+ if (!responseJSON || !responseJSON.status) {
+ return makeRespErr({ message: ERR_UNEXPECTED });
+ }
+
+ return responseJSON;
+ },
+ [getSetting]
+ );
+
+ //Автоматическая отмена при размонтировании хука
+ React.useEffect(
+ () => () => {
+ abort();
+ },
+ [abort]
+ );
+
+ return {
+ executeAction,
+ abort,
+ isRespErr,
+ getRespErrMessage,
+ RESP_STATUS_OK,
+ RESP_STATUS_ERR
+ };
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useAppServer;
diff --git a/rn/app/src/hooks/useHardwareBackPress.js b/rn/app/src/hooks/useHardwareBackPress.js
new file mode 100644
index 0000000..2a6da9d
--- /dev/null
+++ b/rn/app/src/hooks/useHardwareBackPress.js
@@ -0,0 +1,48 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук обработки аппаратной кнопки "Назад" (Android)
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { BackHandler, Platform } = require('react-native'); //BackHandler и платформа
+
+//-----------
+//Тело модуля
+//-----------
+
+//Хук обработки аппаратной кнопки "Назад"
+const useHardwareBackPress = (handler, deps = []) => {
+ React.useEffect(() => {
+ //BackHandler работает только на Android
+ if (Platform.OS !== 'android') {
+ return;
+ }
+
+ //Обработчик нажатия кнопки "Назад"
+ const backHandler = () => {
+ if (typeof handler === 'function') {
+ return handler();
+ }
+ return false;
+ };
+
+ //Подписка на событие
+ const subscription = BackHandler.addEventListener('hardwareBackPress', backHandler);
+
+ //Отписка при размонтировании
+ return () => {
+ subscription.remove();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, deps);
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useHardwareBackPress;
diff --git a/rn/app/src/hooks/useLocalDb.js b/rn/app/src/hooks/useLocalDb.js
new file mode 100644
index 0000000..8d32e07
--- /dev/null
+++ b/rn/app/src/hooks/useLocalDb.js
@@ -0,0 +1,274 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук для работы с локальной базой данных SQLite
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react');
+const SQLiteDatabase = require('../database/SQLiteDatabase');
+
+//-----------
+//Тело модуля
+//-----------
+
+function useLocalDb() {
+ //Состояние хука
+ const [isDbReady, setIsDbReady] = React.useState(false);
+ const [inspections, setInspections] = React.useState([]);
+ const [dbError, setDbError] = React.useState(null);
+
+ //Отслеживания статуса инициализации
+ const initializingRef = React.useRef(false);
+
+ //Инициализация базы данных при монтировании
+ React.useEffect(() => {
+ let mounted = true;
+
+ //Функция инициализации БД
+ const initDb = async () => {
+ //Предотвращаем повторную инициализацию
+ if (initializingRef.current || SQLiteDatabase.isInitialized) {
+ if (mounted && SQLiteDatabase.isInitialized) {
+ setIsDbReady(true);
+ try {
+ const loadedInspections = await SQLiteDatabase.getInspections();
+ setInspections(loadedInspections);
+ } catch (loadError) {
+ console.error('Ошибка загрузки осмотров при переинициализации:', loadError);
+ }
+ }
+ return;
+ }
+
+ initializingRef.current = true;
+
+ try {
+ await SQLiteDatabase.initialize();
+ if (mounted) {
+ setIsDbReady(true);
+ const loadedInspections = await SQLiteDatabase.getInspections();
+ setInspections(loadedInspections);
+ setDbError(null);
+ }
+ } catch (initError) {
+ console.error('Ошибка инициализации базы данных:', initError);
+ if (mounted) {
+ setIsDbReady(false);
+ setDbError('Не удалось инициализировать базу данных');
+ }
+ } finally {
+ initializingRef.current = false;
+ }
+ };
+
+ initDb();
+
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ //Загрузка списка осмотров
+ const loadInspections = React.useCallback(async () => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return [];
+ }
+
+ try {
+ const loadedInspections = await SQLiteDatabase.getInspections();
+ setInspections(loadedInspections);
+ setDbError(null);
+ return loadedInspections;
+ } catch (loadError) {
+ console.error('Ошибка загрузки осмотров:', loadError);
+ setDbError('Не удалось загрузить осмотры');
+ return [];
+ }
+ }, [isDbReady]);
+
+ //Сохранение осмотра
+ const saveInspection = React.useCallback(
+ async inspection => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return inspection;
+ }
+
+ try {
+ await SQLiteDatabase.saveInspection(inspection);
+ const updatedInspections = await SQLiteDatabase.getInspections();
+ setInspections(updatedInspections);
+ setDbError(null);
+ return inspection;
+ } catch (saveError) {
+ console.error('Ошибка сохранения осмотра:', saveError);
+ setDbError('Не удалось сохранить осмотр');
+ return inspection;
+ }
+ },
+ [isDbReady]
+ );
+
+ //Получение настройки
+ const getSetting = React.useCallback(
+ async key => {
+ if (!isDbReady) {
+ return null;
+ }
+
+ try {
+ return await SQLiteDatabase.getSetting(key);
+ } catch (getSettingError) {
+ console.error('Ошибка получения настройки:', getSettingError);
+ return null;
+ }
+ },
+ [isDbReady]
+ );
+
+ //Сохранение настройки
+ const setSetting = React.useCallback(
+ async (key, value) => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ return await SQLiteDatabase.setSetting(key, value);
+ } catch (setSettingError) {
+ console.error('Ошибка сохранения настройки:', setSettingError);
+ return false;
+ }
+ },
+ [isDbReady]
+ );
+
+ //Удаление настройки
+ const deleteSetting = React.useCallback(
+ async key => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ return await SQLiteDatabase.deleteSetting(key);
+ } catch (deleteSettingError) {
+ console.error('Ошибка удаления настройки:', deleteSettingError);
+ return false;
+ }
+ },
+ [isDbReady]
+ );
+
+ //Получение всех настроек
+ const getAllSettings = React.useCallback(async () => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return {};
+ }
+
+ try {
+ return await SQLiteDatabase.getAllSettings();
+ } catch (getAllSettingsError) {
+ console.error('Ошибка получения всех настроек:', getAllSettingsError);
+ return {};
+ }
+ }, [isDbReady]);
+
+ //Очистка всех настроек
+ const clearSettings = React.useCallback(async () => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ return await SQLiteDatabase.clearSettings();
+ } catch (clearSettingsError) {
+ console.error('Ошибка очистки настроек:', clearSettingsError);
+ return false;
+ }
+ }, [isDbReady]);
+
+ //Очистка всех осмотров
+ const clearInspections = React.useCallback(async () => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ const result = await SQLiteDatabase.clearInspections();
+ if (result) {
+ setInspections([]);
+ setDbError(null);
+ }
+ return result;
+ } catch (clearInspectionsError) {
+ console.error('Ошибка очистки осмотров:', clearInspectionsError);
+ setDbError('Не удалось очистить осмотры');
+ return false;
+ }
+ }, [isDbReady]);
+
+ //Оптимизация базы данных
+ const vacuum = React.useCallback(async () => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ return await SQLiteDatabase.vacuum();
+ } catch (vacuumError) {
+ console.error('Ошибка оптимизации базы данных:', vacuumError);
+ return false;
+ }
+ }, [isDbReady]);
+
+ //Проверка существования таблицы
+ const checkTableExists = React.useCallback(
+ async tableName => {
+ if (!isDbReady) {
+ console.warn('База данных не готова');
+ return false;
+ }
+
+ try {
+ return await SQLiteDatabase.checkTableExists(tableName);
+ } catch (checkTableError) {
+ console.error('Ошибка проверки существования таблицы:', checkTableError);
+ return false;
+ }
+ },
+ [isDbReady]
+ );
+
+ return {
+ isDbReady,
+ inspections,
+ error: dbError,
+ loadInspections,
+ saveInspection,
+ getSetting,
+ setSetting,
+ deleteSetting,
+ getAllSettings,
+ clearSettings,
+ clearInspections,
+ vacuum,
+ checkTableExists
+ };
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = useLocalDb;
diff --git a/rn/app/src/hooks/usePreTripInspections.js b/rn/app/src/hooks/usePreTripInspections.js
new file mode 100644
index 0000000..e9111bd
--- /dev/null
+++ b/rn/app/src/hooks/usePreTripInspections.js
@@ -0,0 +1,147 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Хук предметной области "Предрейсовые осмотры"
+
+ Объединяет:
+ - работу с сервером приложений;
+ - работу с локальной базой данных;
+ - управление состоянием загрузки/ошибок.
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const useAppServer = require('./useAppServer'); //Хук для сервера приложений
+const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
+const useAppMode = require('./useAppMode'); //Хук режима работы приложения
+
+//---------
+//Константы
+//---------
+
+//Статусы загрузки данных
+const LOAD_STATUS_IDLE = 'IDLE';
+const LOAD_STATUS_LOADING = 'LOADING';
+const LOAD_STATUS_DONE = 'DONE';
+const LOAD_STATUS_ERROR = 'ERROR';
+
+//-----------
+//Тело модуля
+//-----------
+
+//Хук предметной области "Предрейсовые осмотры"
+function usePreTripInspections() {
+ const { executeAction, isRespErr, getRespErrMessage, RESP_STATUS_OK } = useAppServer();
+ const { inspections, loadInspections, saveInspection, isDbReady } = useAppLocalDbContext();
+ const { APP_MODE, mode } = useAppMode();
+
+ const [loadStatus, setLoadStatus] = React.useState(LOAD_STATUS_IDLE);
+ const [error, setError] = React.useState(null);
+
+ //Загрузка списка осмотров:
+ //1) Если режим OFFLINE - работаем только с локальной БД;
+ //2) Если режим ONLINE - пробуем получить данные с сервера приложений,
+ // при ошибке используем локальные данные.
+ const refreshInspections = React.useCallback(async () => {
+ //Проверяем готовность базы данных
+ if (!isDbReady) {
+ return {
+ inspections: [],
+ fromServer: false
+ };
+ }
+
+ setLoadStatus(LOAD_STATUS_LOADING);
+ setError(null);
+
+ //Режим OFFLINE - работаем только с локальными данными
+ if (mode === APP_MODE.OFFLINE) {
+ const localInspections = await loadInspections();
+ setLoadStatus(LOAD_STATUS_DONE);
+ return {
+ inspections: localInspections,
+ fromServer: false
+ };
+ }
+
+ //Режим ONLINE - пробуем получить данные с сервера приложений
+ const serverResponse = await executeAction({
+ path: 'api/pretrip/inspections',
+ method: 'POST',
+ payload: {}
+ });
+
+ //Ошибка сервера приложений - пробуем взять данные из локальной БД
+ if (isRespErr(serverResponse) || serverResponse.status !== RESP_STATUS_OK) {
+ const localInspections = await loadInspections();
+ setLoadStatus(localInspections.length > 0 ? LOAD_STATUS_DONE : LOAD_STATUS_ERROR);
+ setError(getRespErrMessage(serverResponse) || 'Не удалось получить данные с сервера приложений');
+ return {
+ inspections: localInspections,
+ fromServer: false
+ };
+ }
+
+ //Успех - приводим полезную нагрузку к ожидаемому виду
+ const payload = serverResponse.payload || {};
+ const resultInspections = Array.isArray(payload.inspections) ? payload.inspections : [];
+
+ setLoadStatus(LOAD_STATUS_DONE);
+
+ return {
+ inspections: resultInspections,
+ fromServer: true
+ };
+ }, [APP_MODE.OFFLINE, RESP_STATUS_OK, executeAction, getRespErrMessage, isDbReady, isRespErr, loadInspections, mode]);
+
+ //Создание или обновление осмотра
+ const upsertInspection = React.useCallback(
+ async inspection => {
+ const safeInspection = {
+ id: inspection.id || `local-${Date.now()}`,
+ title: inspection.title || 'Новый осмотр',
+ status: inspection.status || 'DRAFT',
+ createdAt: inspection.createdAt || new Date().toISOString()
+ };
+
+ //Сохраняем в локальной БД (всегда, независимо от режима)
+ await saveInspection(safeInspection);
+
+ //Если приложение в режиме ONLINE - отправляем данные на сервер приложений
+ //TODO: вызов конкретного метода сервера
+ if (mode === APP_MODE.ONLINE) {
+ await executeAction({
+ path: 'api/pretrip/inspections/save',
+ method: 'POST',
+ payload: { inspection: safeInspection }
+ });
+ }
+
+ return safeInspection;
+ },
+ [APP_MODE.ONLINE, executeAction, saveInspection, mode]
+ );
+
+ return {
+ inspections,
+ loadStatus,
+ error,
+ isDbReady,
+ refreshInspections,
+ upsertInspection
+ };
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ usePreTripInspections,
+ LOAD_STATUS_IDLE,
+ LOAD_STATUS_LOADING,
+ LOAD_STATUS_DONE,
+ LOAD_STATUS_ERROR
+};
diff --git a/rn/app/src/screens/MainScreen.js b/rn/app/src/screens/MainScreen.js
new file mode 100644
index 0000000..1a22e8f
--- /dev/null
+++ b/rn/app/src/screens/MainScreen.js
@@ -0,0 +1,136 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Главный экран приложения с боковым меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react'); //React и хуки
+const { View } = require('react-native'); //Базовые компоненты
+const { LOAD_STATUS_LOADING } = require('../hooks/usePreTripInspections'); //Константы загрузки
+const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
+const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
+const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
+const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
+const { useAppPreTripInspectionsContext } = require('../components/layout/AppPreTripInspectionsProvider'); //Контекст осмотров
+const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
+const SideMenu = require('../components/menu/SideMenu'); //Боковое меню
+const InspectionList = require('../components/inspections/InspectionList'); //Список осмотров
+const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
+const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана
+
+//-----------
+//Тело модуля
+//-----------
+
+//Главный экран приложения
+function MainScreen() {
+ const { inspections, loadStatus, error, isDbReady, refreshInspections } = useAppPreTripInspectionsContext();
+ const { showInfo } = useAppMessagingContext();
+ const { mode } = useAppModeContext();
+ const { navigate, SCREENS } = useAppNavigationContext();
+ const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
+
+ const [menuVisible, setMenuVisible] = React.useState(false);
+ const [serverUrl, setServerUrl] = React.useState('');
+
+ //Предотвращение повторной загрузки при монтировании
+ const initialLoadRef = React.useRef(false);
+
+ //Загрузка URL сервера при готовности БД
+ React.useEffect(() => {
+ if (!isLocalDbReady) {
+ return;
+ }
+
+ const loadServerUrl = async () => {
+ try {
+ const savedUrl = await getSetting('app_server_url');
+ if (savedUrl) {
+ setServerUrl(savedUrl);
+ }
+ } catch (loadError) {
+ console.error('Ошибка загрузки URL сервера:', loadError);
+ }
+ };
+
+ loadServerUrl();
+ }, [isLocalDbReady, getSetting]);
+
+ //Первичная загрузка данных
+ React.useEffect(() => {
+ //Выходим, если БД не готова или уже загружали
+ if (!isDbReady || initialLoadRef.current) {
+ return;
+ }
+ initialLoadRef.current = true;
+ refreshInspections();
+ }, [isDbReady, refreshInspections]);
+
+ //Обработчик открытия меню
+ const handleMenuOpen = React.useCallback(() => {
+ setMenuVisible(true);
+ }, []);
+
+ //Обработчик закрытия меню
+ const handleMenuClose = React.useCallback(() => {
+ setMenuVisible(false);
+ }, []);
+
+ //Обработчик показа информации о приложении
+ const handleShowAbout = React.useCallback(() => {
+ const appInfo = getAppInfo({
+ mode,
+ serverUrl,
+ isDbReady: isLocalDbReady
+ });
+
+ showInfo(appInfo, {
+ title: 'Информация о приложении'
+ });
+ }, [showInfo, mode, serverUrl, isLocalDbReady]);
+
+ //Пункты бокового меню
+ const menuItems = React.useMemo(
+ () => [
+ {
+ id: 'settings',
+ title: 'Настройки',
+ onPress: () => {
+ navigate(SCREENS.SETTINGS);
+ }
+ },
+ {
+ id: 'about',
+ title: 'О приложении',
+ onPress: handleShowAbout
+ }
+ ],
+ [navigate, handleShowAbout, SCREENS.SETTINGS]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = MainScreen;
diff --git a/rn/app/src/screens/SettingsScreen.js b/rn/app/src/screens/SettingsScreen.js
new file mode 100644
index 0000000..b53a9b9
--- /dev/null
+++ b/rn/app/src/screens/SettingsScreen.js
@@ -0,0 +1,379 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Экран настроек приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const React = require('react');
+const { ScrollView, View, Pressable } = require('react-native');
+const AdaptiveView = require('../components/common/AdaptiveView');
+const AppText = require('../components/common/AppText');
+const AppButton = require('../components/common/AppButton');
+const CopyButton = require('../components/common/CopyButton');
+const InputDialog = require('../components/common/InputDialog');
+const AppHeader = require('../components/layout/AppHeader');
+const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider');
+const { useAppModeContext } = require('../components/layout/AppModeProvider');
+const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider');
+const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider');
+const { APP_COLORS } = require('../config/theme');
+const { getAppInfo, getModeLabel } = require('../utils/appInfo');
+const styles = require('../styles/screens/SettingsScreen.styles');
+
+//-----------
+//Тело модуля
+//-----------
+
+function SettingsScreen() {
+ const { showInfo, showError, showSuccess } = useAppMessagingContext();
+ const { mode, setOnline, setNotConnected } = useAppModeContext();
+ const { goBack, canGoBack } = useAppNavigationContext();
+ const { getSetting, setSetting, clearSettings, clearInspections, vacuum, isDbReady } = useAppLocalDbContext();
+
+ const [serverUrl, setServerUrl] = React.useState('');
+ const [isLoading, setIsLoading] = React.useState(false);
+ const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
+
+ //Предотвращение повторной загрузки настроек
+ const settingsLoadedRef = React.useRef(false);
+
+ //Загрузка сохраненного URL сервера при готовности БД
+ React.useEffect(() => {
+ //Выходим, если БД не готова или уже загрузили настройки
+ if (!isDbReady || settingsLoadedRef.current) {
+ return;
+ }
+
+ settingsLoadedRef.current = true;
+
+ const loadSettings = async () => {
+ setIsLoading(true);
+ try {
+ const savedUrl = await getSetting('app_server_url');
+ if (savedUrl) {
+ setServerUrl(savedUrl);
+ }
+ } catch (error) {
+ console.error('Ошибка загрузки настроек:', error);
+ showError('Не удалось загрузить настройки');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadSettings();
+ }, [isDbReady, getSetting, showError]);
+
+ //Открытие диалога ввода URL сервера
+ const handleOpenServerUrlDialog = React.useCallback(() => {
+ setIsServerUrlDialogVisible(true);
+ }, []);
+
+ //Закрытие диалога ввода URL сервера
+ const handleCloseServerUrlDialog = React.useCallback(() => {
+ setIsServerUrlDialogVisible(false);
+ }, []);
+
+ //Валидатор URL сервера
+ const validateServerUrl = React.useCallback(url => {
+ if (!url || !url.trim()) {
+ return 'Введите адрес сервера';
+ }
+
+ try {
+ const parsedUrl = new URL(url.trim());
+ if (!parsedUrl.protocol.startsWith('http')) {
+ return 'Используйте http:// или https:// протокол';
+ }
+ } catch (error) {
+ return 'Некорректный формат URL';
+ }
+
+ return true;
+ }, []);
+
+ //Сохранение настроек сервера
+ const handleSaveServerUrl = React.useCallback(
+ async url => {
+ setIsServerUrlDialogVisible(false);
+ setIsLoading(true);
+
+ try {
+ const success = await setSetting('app_server_url', url);
+
+ if (success) {
+ setServerUrl(url);
+ showSuccess('Настройки сервера сохранены');
+
+ if (mode === 'NOT_CONNECTED') {
+ setOnline();
+ }
+ } else {
+ showError('Не удалось сохранить настройки');
+ }
+ } catch (error) {
+ console.error('Ошибка сохранения настроек:', error);
+ showError('Не удалось сохранить настройки');
+ }
+
+ setIsLoading(false);
+ },
+ [mode, setOnline, setSetting, showError, showSuccess]
+ );
+
+ //Очистка кэша (осмотров)
+ const handleClearCache = React.useCallback(async () => {
+ showInfo('Очистить кэш приложения?', {
+ title: 'Подтверждение',
+ buttons: [
+ {
+ id: 'cancel',
+ title: 'Отмена',
+ onPress: () => { }
+ },
+ {
+ id: 'confirm',
+ title: 'Очистить',
+ onPress: async () => {
+ try {
+ const success = await clearInspections();
+ if (success) {
+ showSuccess('Кэш успешно очищен');
+ } else {
+ showError('Не удалось очистить кэш');
+ }
+ } catch (error) {
+ console.error('Ошибка очистки кэша:', error);
+ showError('Не удалось очистить кэш');
+ }
+ },
+ buttonStyle: { backgroundColor: APP_COLORS.error },
+ textStyle: { color: APP_COLORS.white }
+ }
+ ]
+ });
+ }, [showInfo, showSuccess, showError, clearInspections]);
+
+ //Сброс всех настроек
+ const handleResetSettings = React.useCallback(async () => {
+ showInfo('Сбросить все настройки к значениям по умолчанию?', {
+ title: 'Подтверждение сброса',
+ buttons: [
+ {
+ id: 'cancel',
+ title: 'Отмена',
+ onPress: () => { }
+ },
+ {
+ id: 'confirm',
+ title: 'Сбросить',
+ onPress: async () => {
+ try {
+ const success = await clearSettings();
+ if (success) {
+ setServerUrl('');
+ setNotConnected();
+ showSuccess('Настройки сброшены');
+ } else {
+ showError('Не удалось сбросить настройки');
+ }
+ } catch (error) {
+ console.error('Ошибка сброса настроек:', error);
+ showError('Не удалось сбросить настройки');
+ }
+ },
+ buttonStyle: { backgroundColor: APP_COLORS.warning },
+ textStyle: { color: APP_COLORS.white }
+ }
+ ]
+ });
+ }, [showInfo, showSuccess, showError, setNotConnected, clearSettings]);
+
+ //Оптимизация базы данных
+ const handleOptimizeDb = React.useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const success = await vacuum();
+ if (success) {
+ showSuccess('База данных оптимизирована');
+ } else {
+ showError('Не удалось оптимизировать базу данных');
+ }
+ } catch (error) {
+ console.error('Ошибка оптимизации БД:', error);
+ showError('Не удалось оптимизировать базу данных');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [vacuum, showSuccess, showError]);
+
+ //Информация о приложении
+ const handleShowAppInfo = React.useCallback(() => {
+ const appInfo = getAppInfo({
+ mode,
+ serverUrl,
+ isDbReady
+ });
+
+ showInfo(appInfo, {
+ title: 'Информация о приложении'
+ });
+ }, [mode, serverUrl, isDbReady, showInfo]);
+
+ //Обработчик кнопки назад
+ const handleBackPress = React.useCallback(() => {
+ if (canGoBack) {
+ goBack();
+ }
+ }, [canGoBack, goBack]);
+
+ //Обработчик копирования адреса сервера
+ const handleCopyServerUrl = React.useCallback(() => {
+ showSuccess('Адрес сервера скопирован');
+ }, [showSuccess]);
+
+ //Обработчик ошибки копирования
+ const handleCopyError = React.useCallback(() => {
+ showError('Не удалось скопировать адрес');
+ }, [showError]);
+
+ return (
+
+
+
+
+
+
+ Сервер приложений
+
+
+
+ URL сервера
+
+
+
+ [styles.serverUrlField, pressed && styles.serverUrlFieldPressed]}
+ onPress={handleOpenServerUrlDialog}
+ disabled={isLoading || !isDbReady}
+ >
+
+ {serverUrl || 'Нажмите для ввода адреса'}
+
+
+
+ {serverUrl ? (
+
+ ) : null}
+
+
+
+
+
+ Управление данными
+
+
+
+
+
+
+
+
+
+
+
+ Информация
+
+
+
+
+
+
+ Текущий режим:
+
+
+ {getModeLabel(mode)}
+
+
+
+
+
+ Адрес сервера:
+
+
+
+ {serverUrl || 'Не настроен'}
+
+ {serverUrl ? (
+
+ ) : null}
+
+
+
+
+
+ База данных:
+
+
+ {isDbReady ? 'Готова' : 'Загрузка...'}
+
+
+
+
+
+
+
+ );
+}
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = SettingsScreen;
diff --git a/rn/app/src/styles/common/AdaptiveView.styles.js b/rn/app/src/styles/common/AdaptiveView.styles.js
new file mode 100644
index 0000000..8d39a82
--- /dev/null
+++ b/rn/app/src/styles/common/AdaptiveView.styles.js
@@ -0,0 +1,33 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили адаптивного контейнера
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили адаптивного контейнера
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: APP_COLORS.background
+ },
+ padding: {
+ paddingHorizontal: UI.PADDING
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/AppButton.styles.js b/rn/app/src/styles/common/AppButton.styles.js
new file mode 100644
index 0000000..6fba5b0
--- /dev/null
+++ b/rn/app/src/styles/common/AppButton.styles.js
@@ -0,0 +1,58 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили общего компонента кнопки
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили кнопки
+const styles = StyleSheet.create({
+ base: {
+ borderRadius: UI.BORDER_RADIUS,
+ backgroundColor: APP_COLORS.primary,
+ overflow: 'hidden',
+ minHeight: UI.BUTTON_HEIGHT,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ pressed: {
+ opacity: 0.85,
+ transform: [{ scale: 0.98 }]
+ },
+ disabled: {
+ backgroundColor: APP_COLORS.primaryLight,
+ opacity: 0.6
+ },
+ content: {
+ paddingHorizontal: responsiveSpacing(4),
+ paddingVertical: responsiveSpacing(2),
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'row'
+ },
+ text: {
+ color: APP_COLORS.white,
+ fontWeight: '600',
+ fontSize: UI.FONT_SIZE_MD,
+ textAlign: 'center',
+ includeFontPadding: false,
+ textAlignVertical: 'center'
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/AppInput.styles.js b/rn/app/src/styles/common/AppInput.styles.js
new file mode 100644
index 0000000..7f4de2a
--- /dev/null
+++ b/rn/app/src/styles/common/AppInput.styles.js
@@ -0,0 +1,73 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента ввода
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили компонента ввода
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: responsiveSpacing(4)
+ },
+ label: {
+ marginBottom: responsiveSpacing(2),
+ color: APP_COLORS.textPrimary
+ },
+ input: {
+ borderWidth: 1,
+ borderColor: APP_COLORS.borderMedium,
+ borderRadius: UI.BORDER_RADIUS,
+ paddingHorizontal: responsiveSpacing(3),
+ paddingVertical: responsiveSpacing(2.5),
+ fontSize: UI.FONT_SIZE_MD,
+ color: APP_COLORS.textPrimary,
+ backgroundColor: APP_COLORS.white,
+ minHeight: UI.INPUT_HEIGHT,
+ includeFontPadding: false,
+ textAlignVertical: 'center'
+ },
+ inputFocused: {
+ borderColor: APP_COLORS.primary,
+ borderWidth: 2,
+ shadowColor: APP_COLORS.primary,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.2,
+ shadowRadius: 4,
+ elevation: 2
+ },
+ inputError: {
+ borderColor: APP_COLORS.error
+ },
+ inputDisabled: {
+ backgroundColor: APP_COLORS.surfaceAlt,
+ color: APP_COLORS.textTertiary
+ },
+ placeholder: {
+ color: APP_COLORS.textTertiary
+ },
+ helperText: {
+ marginTop: responsiveSpacing(1),
+ color: APP_COLORS.textSecondary
+ },
+ helperTextError: {
+ color: APP_COLORS.error
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/AppLogo.styles.js b/rn/app/src/styles/common/AppLogo.styles.js
new file mode 100644
index 0000000..d72bc1b
--- /dev/null
+++ b/rn/app/src/styles/common/AppLogo.styles.js
@@ -0,0 +1,135 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента иконки/логотипа приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Цвета иконки
+const TEAL_BACKGROUND = '#50AF95';
+const ROBOT_WHITE = '#FFFFFF';
+
+//Размеры для разных вариантов
+const SIZES = {
+ small: responsiveSize(32),
+ medium: responsiveSize(40),
+ large: responsiveSize(56)
+};
+
+//Стили логотипа
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: TEAL_BACKGROUND,
+ overflow: 'hidden'
+ },
+ containerSmall: {
+ width: SIZES.small,
+ height: SIZES.small,
+ borderRadius: responsiveSize(6)
+ },
+ containerMedium: {
+ width: SIZES.medium,
+ height: SIZES.medium,
+ borderRadius: responsiveSize(8)
+ },
+ containerLarge: {
+ width: SIZES.large,
+ height: SIZES.large,
+ borderRadius: responsiveSize(10)
+ },
+ robotContainer: {
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ antennasContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginBottom: responsiveSize(-1)
+ },
+ antenna: {
+ backgroundColor: ROBOT_WHITE,
+ borderRadius: responsiveSize(2)
+ },
+ antennaLeft: {
+ transform: [{ rotate: '-30deg' }],
+ marginRight: responsiveSize(4)
+ },
+ antennaRight: {
+ transform: [{ rotate: '30deg' }],
+ marginLeft: responsiveSize(4)
+ },
+ antennaSmall: {
+ width: responsiveSize(1.5),
+ height: responsiveSize(4)
+ },
+ antennaMedium: {
+ width: responsiveSize(2),
+ height: responsiveSize(5)
+ },
+ antennaLarge: {
+ width: responsiveSize(2.5),
+ height: responsiveSize(7)
+ },
+ head: {
+ backgroundColor: ROBOT_WHITE,
+ borderTopLeftRadius: responsiveSize(100),
+ borderTopRightRadius: responsiveSize(100),
+ borderBottomLeftRadius: responsiveSize(4),
+ borderBottomRightRadius: responsiveSize(4),
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ headSmall: {
+ width: responsiveSize(18),
+ height: responsiveSize(10),
+ paddingTop: responsiveSize(3)
+ },
+ headMedium: {
+ width: responsiveSize(22),
+ height: responsiveSize(12),
+ paddingTop: responsiveSize(4)
+ },
+ headLarge: {
+ width: responsiveSize(32),
+ height: responsiveSize(18),
+ paddingTop: responsiveSize(5)
+ },
+ eyesContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ width: '50%'
+ },
+ eye: {
+ backgroundColor: TEAL_BACKGROUND,
+ borderRadius: responsiveSize(100)
+ },
+ eyeSmall: {
+ width: responsiveSize(2),
+ height: responsiveSize(2)
+ },
+ eyeMedium: {
+ width: responsiveSize(2.5),
+ height: responsiveSize(2.5)
+ },
+ eyeLarge: {
+ width: responsiveSize(4),
+ height: responsiveSize(4)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/AppMessage.styles.js b/rn/app/src/styles/common/AppMessage.styles.js
new file mode 100644
index 0000000..c8516e2
--- /dev/null
+++ b/rn/app/src/styles/common/AppMessage.styles.js
@@ -0,0 +1,115 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили общего компонента сообщения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require("react-native"); //StyleSheet React Native
+const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили сообщения
+const styles = StyleSheet.create({
+ backdrop: {
+ flex: 1,
+ backgroundColor: APP_COLORS.overlay,
+ alignItems: "center",
+ justifyContent: "center",
+ padding: 24
+ },
+ container: {
+ width: "100%",
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: APP_COLORS.surface,
+ shadowColor: APP_COLORS.black,
+ shadowOpacity: 0.15,
+ shadowRadius: 12,
+ shadowOffset: {
+ width: 0,
+ height: 4
+ },
+ elevation: 8
+ },
+ containerInfo: {
+ borderLeftWidth: 4,
+ borderLeftColor: APP_COLORS.primary
+ },
+ containerWarn: {
+ borderLeftWidth: 4,
+ borderLeftColor: APP_COLORS.warning
+ },
+ containerError: {
+ borderLeftWidth: 4,
+ borderLeftColor: APP_COLORS.error
+ },
+ containerSuccess: {
+ borderLeftWidth: 4,
+ borderLeftColor: APP_COLORS.success
+ },
+ header: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginBottom: 8
+ },
+ title: {
+ flex: 1,
+ fontSize: 16,
+ fontWeight: "600",
+ color: APP_COLORS.textPrimary,
+ marginRight: 8
+ },
+ closeButton: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ alignItems: "center",
+ justifyContent: "center"
+ },
+ closeButtonText: {
+ fontSize: 20,
+ lineHeight: 20,
+ color: APP_COLORS.textSecondary
+ },
+ content: {
+ marginBottom: 12
+ },
+ message: {
+ fontSize: 14,
+ color: APP_COLORS.textSecondary
+ },
+ buttonsRow: {
+ flexDirection: "row",
+ justifyContent: "flex-end",
+ gap: 8
+ },
+ buttonBase: {
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: 999,
+ backgroundColor: APP_COLORS.primary
+ },
+ buttonPressed: {
+ opacity: 0.85
+ },
+ buttonText: {
+ fontSize: 14,
+ fontWeight: "500",
+ color: APP_COLORS.white
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
+
diff --git a/rn/app/src/styles/common/AppText.styles.js b/rn/app/src/styles/common/AppText.styles.js
new file mode 100644
index 0000000..1587545
--- /dev/null
+++ b/rn/app/src/styles/common/AppText.styles.js
@@ -0,0 +1,31 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили общего текстового компонента
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили текста по умолчанию
+const styles = StyleSheet.create({
+ text: {
+ color: APP_COLORS.textPrimary,
+ fontSize: responsiveSize(16),
+ includeFontPadding: false
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/Backdrop.styles.js b/rn/app/src/styles/common/Backdrop.styles.js
new file mode 100644
index 0000000..3ac2508
--- /dev/null
+++ b/rn/app/src/styles/common/Backdrop.styles.js
@@ -0,0 +1,34 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента заднего фона
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили заднего фона
+const styles = StyleSheet.create({
+ backdrop: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: APP_COLORS.overlay,
+ zIndex: 999
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/CopyButton.styles.js b/rn/app/src/styles/common/CopyButton.styles.js
new file mode 100644
index 0000000..5cf02fc
--- /dev/null
+++ b/rn/app/src/styles/common/CopyButton.styles.js
@@ -0,0 +1,74 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента кнопки копирования
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Размеры кнопки и иконки
+const BUTTON_SIZE = responsiveSize(36);
+const ICON_SIZE = responsiveSize(16);
+const ICON_OFFSET = responsiveSize(3);
+
+//Стили кнопки копирования
+const styles = StyleSheet.create({
+ button: {
+ width: BUTTON_SIZE,
+ height: BUTTON_SIZE,
+ borderRadius: responsiveSize(8),
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.primary + '15',
+ borderWidth: 1,
+ borderColor: APP_COLORS.primary + '30'
+ },
+ buttonPressed: {
+ backgroundColor: APP_COLORS.primary + '30'
+ },
+ buttonDisabled: {
+ opacity: 0.5
+ },
+ iconContainer: {
+ width: ICON_SIZE,
+ height: ICON_SIZE,
+ position: 'relative'
+ },
+ iconBack: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: ICON_SIZE - ICON_OFFSET,
+ height: ICON_SIZE - ICON_OFFSET,
+ borderWidth: responsiveSize(1.5),
+ borderColor: APP_COLORS.primary,
+ borderRadius: responsiveSize(2),
+ backgroundColor: 'transparent'
+ },
+ iconFront: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ width: ICON_SIZE - ICON_OFFSET,
+ height: ICON_SIZE - ICON_OFFSET,
+ borderWidth: responsiveSize(1.5),
+ borderColor: APP_COLORS.primary,
+ borderRadius: responsiveSize(2),
+ backgroundColor: APP_COLORS.surface
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/common/InputDialog.styles.js b/rn/app/src/styles/common/InputDialog.styles.js
new file mode 100644
index 0000000..5a82ee6
--- /dev/null
+++ b/rn/app/src/styles/common/InputDialog.styles.js
@@ -0,0 +1,133 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента модального окна с полем ввода
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet, Platform } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveSize, responsiveSpacing, widthPercentage } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили диалога ввода
+const styles = StyleSheet.create({
+ backdrop: {
+ flex: 1,
+ backgroundColor: APP_COLORS.overlay,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: UI.PADDING
+ },
+ container: {
+ width: '100%',
+ maxWidth: widthPercentage(90),
+ backgroundColor: APP_COLORS.surface,
+ borderRadius: UI.BORDER_RADIUS,
+ ...Platform.select({
+ ios: {
+ shadowColor: APP_COLORS.black,
+ shadowOpacity: 0.25,
+ shadowRadius: 10,
+ shadowOffset: { width: 0, height: 4 }
+ },
+ android: {
+ elevation: 10
+ },
+ web: {
+ boxShadow: '0 4px 10px rgba(0, 0, 0, 0.25)'
+ }
+ })
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: responsiveSpacing(3),
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle
+ },
+ title: {
+ fontSize: UI.FONT_SIZE_LG,
+ fontWeight: '600',
+ color: APP_COLORS.textPrimary,
+ flex: 1
+ },
+ closeButton: {
+ width: responsiveSize(32),
+ height: responsiveSize(32),
+ borderRadius: responsiveSize(16),
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.surfaceAlt
+ },
+ closeButtonText: {
+ fontSize: responsiveSize(24),
+ color: APP_COLORS.textSecondary,
+ fontWeight: '300',
+ lineHeight: responsiveSize(32)
+ },
+ content: {
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: responsiveSpacing(4)
+ },
+ label: {
+ marginBottom: responsiveSpacing(2),
+ color: APP_COLORS.textSecondary
+ },
+ input: {
+ height: UI.INPUT_HEIGHT,
+ borderWidth: 1,
+ borderColor: APP_COLORS.borderMedium,
+ borderRadius: UI.BORDER_RADIUS,
+ paddingHorizontal: responsiveSpacing(3),
+ fontSize: UI.FONT_SIZE_MD,
+ color: APP_COLORS.textPrimary,
+ backgroundColor: APP_COLORS.surface
+ },
+ inputFocused: {
+ borderColor: APP_COLORS.primary,
+ borderWidth: 2
+ },
+ inputError: {
+ borderColor: APP_COLORS.error
+ },
+ placeholder: {
+ color: APP_COLORS.textTertiary
+ },
+ errorText: {
+ color: APP_COLORS.error,
+ marginTop: responsiveSpacing(1.5)
+ },
+ buttonsRow: {
+ flexDirection: 'row',
+ paddingHorizontal: UI.PADDING,
+ paddingBottom: responsiveSpacing(4),
+ gap: responsiveSpacing(3)
+ },
+ cancelButton: {
+ flex: 1,
+ backgroundColor: APP_COLORS.surfaceAlt,
+ borderWidth: 1,
+ borderColor: APP_COLORS.borderMedium
+ },
+ cancelButtonText: {
+ color: APP_COLORS.textSecondary
+ },
+ confirmButton: {
+ flex: 1
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/inspections/InspectionItem.styles.js b/rn/app/src/styles/inspections/InspectionItem.styles.js
new file mode 100644
index 0000000..7b276e8
--- /dev/null
+++ b/rn/app/src/styles/inspections/InspectionItem.styles.js
@@ -0,0 +1,46 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили элемента списка предрейсовых осмотров
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require("react-native"); //StyleSheet React Native
+const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили элемента
+const styles = StyleSheet.create({
+ container: {
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ backgroundColor: APP_COLORS.surface
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: "500",
+ marginBottom: 4
+ },
+ metaRow: {
+ flexDirection: "row",
+ justifyContent: "space-between"
+ },
+ meta: {
+ fontSize: 12,
+ color: APP_COLORS.textSecondary
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
+
diff --git a/rn/app/src/styles/inspections/InspectionList.styles.js b/rn/app/src/styles/inspections/InspectionList.styles.js
new file mode 100644
index 0000000..a09f225
--- /dev/null
+++ b/rn/app/src/styles/inspections/InspectionList.styles.js
@@ -0,0 +1,46 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили списка предрейсовых осмотров
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require("react-native"); //StyleSheet React Native
+const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили списка
+const styles = StyleSheet.create({
+ centerContainer: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ paddingHorizontal: 32
+ },
+ centerText: {
+ marginTop: 12,
+ textAlign: "center",
+ color: APP_COLORS.textSecondary
+ },
+ errorText: {
+ marginTop: 8,
+ textAlign: "center",
+ color: APP_COLORS.error,
+ fontSize: 12
+ },
+ centerButton: {
+ marginTop: 16
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
+
diff --git a/rn/app/src/styles/layout/AppErrorBoundary.styles.js b/rn/app/src/styles/layout/AppErrorBoundary.styles.js
new file mode 100644
index 0000000..cb40931
--- /dev/null
+++ b/rn/app/src/styles/layout/AppErrorBoundary.styles.js
@@ -0,0 +1,63 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента обработки ошибок приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require("react-native"); //StyleSheet React Native
+const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили страницы ошибки
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: APP_COLORS.background,
+ padding: 24
+ },
+ card: {
+ width: "100%",
+ borderRadius: 12,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: APP_COLORS.surface,
+ shadowColor: APP_COLORS.black,
+ shadowOpacity: 0.1,
+ shadowRadius: 10,
+ shadowOffset: {
+ width: 0,
+ height: 4
+ },
+ elevation: 6
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: APP_COLORS.error,
+ marginBottom: 8
+ },
+ message: {
+ fontSize: 14,
+ color: APP_COLORS.textSecondary,
+ marginBottom: 16
+ },
+ buttonRow: {
+ flexDirection: "row",
+ justifyContent: "flex-end"
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
+
diff --git a/rn/app/src/styles/layout/AppHeader.styles.js b/rn/app/src/styles/layout/AppHeader.styles.js
new file mode 100644
index 0000000..f27d684
--- /dev/null
+++ b/rn/app/src/styles/layout/AppHeader.styles.js
@@ -0,0 +1,174 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента заголовка
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveSize, responsiveWidth, responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили заголовка
+const styles = StyleSheet.create({
+ container: {
+ backgroundColor: APP_COLORS.surface,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ paddingHorizontal: UI.PADDING,
+ height: UI.HEADER_HEIGHT,
+ justifyContent: 'center',
+ shadowColor: APP_COLORS.black,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 3,
+ elevation: 4,
+ zIndex: 10
+ },
+ content: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flex: 1
+ },
+ titleContainer: {
+ flex: 1,
+ marginRight: responsiveSpacing(3),
+ justifyContent: 'center'
+ },
+ title: {
+ fontSize: UI.FONT_SIZE_LG,
+ fontWeight: '600',
+ color: APP_COLORS.textPrimary,
+ marginBottom: responsiveSpacing(1)
+ },
+ subtitle: {
+ fontSize: UI.FONT_SIZE_SM,
+ color: APP_COLORS.textSecondary,
+ includeFontPadding: false
+ },
+ logo: {
+ marginRight: responsiveSpacing(2)
+ },
+ backButton: {
+ width: responsiveSize(40),
+ height: responsiveSize(40),
+ borderRadius: responsiveSize(20),
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.primaryLight,
+ marginRight: responsiveSpacing(2)
+ },
+ backButtonPressed: {
+ backgroundColor: APP_COLORS.primary
+ },
+ backArrowIcon: {
+ width: responsiveSize(16),
+ height: responsiveSize(16),
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ backArrowLineTop: {
+ position: 'absolute',
+ width: responsiveSize(10),
+ height: responsiveSize(2),
+ backgroundColor: APP_COLORS.primary,
+ borderRadius: responsiveSize(1),
+ transform: [{ rotate: '-45deg' }, { translateY: responsiveSize(-2.5) }]
+ },
+ backArrowLineBottom: {
+ position: 'absolute',
+ width: responsiveSize(10),
+ height: responsiveSize(2),
+ backgroundColor: APP_COLORS.primary,
+ borderRadius: responsiveSize(1),
+ transform: [{ rotate: '45deg' }, { translateY: responsiveSize(2.5) }]
+ },
+ controls: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: responsiveSpacing(2)
+ },
+ modeContainer: {
+ paddingHorizontal: responsiveSpacing(3),
+ paddingVertical: responsiveSpacing(1.5),
+ borderRadius: responsiveSize(20),
+ minWidth: responsiveWidth(100),
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ modePressed: {
+ opacity: 0.8
+ },
+ modeOnline: {
+ backgroundColor: APP_COLORS.primary + '15',
+ borderWidth: 1,
+ borderColor: APP_COLORS.primary
+ },
+ modeOffline: {
+ backgroundColor: APP_COLORS.warning + '15',
+ borderWidth: 1,
+ borderColor: APP_COLORS.warning
+ },
+ modeNotConnected: {
+ backgroundColor: APP_COLORS.textSecondary + '15',
+ borderWidth: 1,
+ borderColor: APP_COLORS.textSecondary
+ },
+ modeUnknown: {
+ backgroundColor: APP_COLORS.error + '15',
+ borderWidth: 1,
+ borderColor: APP_COLORS.error
+ },
+ modeText: {
+ fontSize: UI.FONT_SIZE_XS,
+ fontWeight: '600',
+ includeFontPadding: false
+ },
+ modeTextOnline: {
+ color: APP_COLORS.primary
+ },
+ modeTextOffline: {
+ color: APP_COLORS.warning
+ },
+ modeTextNotConnected: {
+ color: APP_COLORS.textSecondary
+ },
+ modeTextUnknown: {
+ color: APP_COLORS.error
+ },
+ menuButton: {
+ width: responsiveSize(40),
+ height: responsiveSize(40),
+ borderRadius: responsiveSize(20),
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.primaryLight
+ },
+ menuButtonPressed: {
+ backgroundColor: APP_COLORS.primary
+ },
+ menuButtonIcon: {
+ width: responsiveSize(20),
+ height: responsiveSize(16),
+ justifyContent: 'space-between'
+ },
+ menuButtonIconLine: {
+ height: responsiveSize(2),
+ backgroundColor: APP_COLORS.primary,
+ borderRadius: responsiveSize(1)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/layout/AppMenu.styles.js b/rn/app/src/styles/layout/AppMenu.styles.js
new file mode 100644
index 0000000..b9b3728
--- /dev/null
+++ b/rn/app/src/styles/layout/AppMenu.styles.js
@@ -0,0 +1,109 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили меню
+const styles = StyleSheet.create({
+ backdrop: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: APP_COLORS.overlay,
+ zIndex: 999
+ },
+ container: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ width: UI.SIDE_MENU_WIDTH,
+ backgroundColor: APP_COLORS.surface,
+ zIndex: 1000,
+ shadowColor: APP_COLORS.black,
+ shadowOpacity: 0.15,
+ shadowRadius: 10,
+ shadowOffset: {
+ width: -2,
+ height: 0
+ },
+ elevation: 10
+ },
+ header: {
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: UI.PADDING,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ backgroundColor: APP_COLORS.primaryLight,
+ height: 60,
+ justifyContent: 'center'
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: APP_COLORS.primaryDark,
+ textAlign: 'center'
+ },
+ content: {
+ flex: 1,
+ paddingTop: UI.PADDING / 2
+ },
+ menuItem: {
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: UI.PADDING,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle
+ },
+ menuItemPressed: {
+ backgroundColor: APP_COLORS.primaryLight
+ },
+ menuItemDestructive: {
+ borderBottomColor: APP_COLORS.error + '20'
+ },
+ menuItemContent: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ menuItemIcon: {
+ marginRight: 12,
+ width: 24,
+ alignItems: 'center'
+ },
+ menuItemText: {
+ fontSize: 16,
+ color: APP_COLORS.textPrimary
+ },
+ menuItemTextDestructive: {
+ color: APP_COLORS.error
+ },
+ emptyMenu: {
+ padding: UI.PADDING * 2,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ emptyMenuText: {
+ fontSize: 14,
+ color: APP_COLORS.textSecondary,
+ textAlign: 'center'
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/layout/AppRoot.styles.js b/rn/app/src/styles/layout/AppRoot.styles.js
new file mode 100644
index 0000000..1a0d33a
--- /dev/null
+++ b/rn/app/src/styles/layout/AppRoot.styles.js
@@ -0,0 +1,29 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили корневого компонента
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили корневого компонента
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: APP_COLORS.background
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/layout/AppShell.styles.js b/rn/app/src/styles/layout/AppShell.styles.js
new file mode 100644
index 0000000..b5ee3af
--- /dev/null
+++ b/rn/app/src/styles/layout/AppShell.styles.js
@@ -0,0 +1,30 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили оболочки приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require("react-native"); //StyleSheet React Native
+const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили оболочки приложения
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: APP_COLORS.background
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
+
diff --git a/rn/app/src/styles/menu/EmptyMenu.styles.js b/rn/app/src/styles/menu/EmptyMenu.styles.js
new file mode 100644
index 0000000..db70e3e
--- /dev/null
+++ b/rn/app/src/styles/menu/EmptyMenu.styles.js
@@ -0,0 +1,38 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента пустого меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { responsiveHeight, responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили пустого меню
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: responsiveHeight(32)
+ },
+ message: {
+ fontSize: responsiveSize(18),
+ color: APP_COLORS.textSecondary,
+ textAlign: 'center',
+ lineHeight: responsiveSize(24)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/menu/MenuDivider.styles.js b/rn/app/src/styles/menu/MenuDivider.styles.js
new file mode 100644
index 0000000..ea0f30f
--- /dev/null
+++ b/rn/app/src/styles/menu/MenuDivider.styles.js
@@ -0,0 +1,32 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента разделителя в меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { responsiveHeight } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили разделителя
+const styles = StyleSheet.create({
+ divider: {
+ height: 1,
+ backgroundColor: APP_COLORS.borderSubtle,
+ marginHorizontal: responsiveHeight(16),
+ marginVertical: responsiveHeight(8)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/menu/MenuHeader.styles.js b/rn/app/src/styles/menu/MenuHeader.styles.js
new file mode 100644
index 0000000..132f97a
--- /dev/null
+++ b/rn/app/src/styles/menu/MenuHeader.styles.js
@@ -0,0 +1,68 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента заголовка меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveHeight, responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили заголовка меню
+const styles = StyleSheet.create({
+ header: {
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: responsiveHeight(16),
+ borderBottomWidth: 1,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ backgroundColor: APP_COLORS.primaryLight,
+ minHeight: responsiveHeight(70),
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ },
+ title: {
+ fontSize: UI.FONT_SIZE_XL,
+ fontWeight: '600',
+ color: APP_COLORS.primaryDark,
+ flex: 1,
+ marginRight: 12
+ },
+ closeButton: {
+ width: responsiveSize(40),
+ height: responsiveSize(40),
+ borderRadius: responsiveSize(20),
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.primary + '20'
+ },
+ closeButtonPressed: {
+ backgroundColor: APP_COLORS.primary + '40'
+ },
+ closeButtonIcon: {
+ width: responsiveSize(40),
+ height: responsiveSize(40),
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ closeButtonText: {
+ fontSize: responsiveSize(28),
+ color: APP_COLORS.primaryDark,
+ fontWeight: '300',
+ lineHeight: responsiveSize(40)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/menu/MenuItem.styles.js b/rn/app/src/styles/menu/MenuItem.styles.js
new file mode 100644
index 0000000..ea0165e
--- /dev/null
+++ b/rn/app/src/styles/menu/MenuItem.styles.js
@@ -0,0 +1,67 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента элемента меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveHeight, responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили элемента меню
+const styles = StyleSheet.create({
+ menuItem: {
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: responsiveHeight(16),
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ minHeight: responsiveHeight(60)
+ },
+ menuItemPressed: {
+ backgroundColor: APP_COLORS.primaryLight + '40'
+ },
+ menuItemDestructive: {
+ borderBottomColor: APP_COLORS.error + '20'
+ },
+ menuItemDisabled: {
+ opacity: 0.5
+ },
+ menuItemContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ flex: 1
+ },
+ menuItemIcon: {
+ marginRight: UI.PADDING,
+ width: responsiveSize(24),
+ height: responsiveSize(24),
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ menuItemText: {
+ fontSize: UI.FONT_SIZE_MD,
+ color: APP_COLORS.textPrimary,
+ flex: 1,
+ fontWeight: '500'
+ },
+ menuItemTextDestructive: {
+ color: APP_COLORS.error
+ },
+ menuItemTextDisabled: {
+ color: APP_COLORS.textSecondary
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/menu/MenuList.styles.js b/rn/app/src/styles/menu/MenuList.styles.js
new file mode 100644
index 0000000..f630d69
--- /dev/null
+++ b/rn/app/src/styles/menu/MenuList.styles.js
@@ -0,0 +1,27 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента списка меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили списка меню
+const styles = StyleSheet.create({
+ scrollView: {
+ flex: 1
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/menu/SideMenu.styles.js b/rn/app/src/styles/menu/SideMenu.styles.js
new file mode 100644
index 0000000..cafc5b2
--- /dev/null
+++ b/rn/app/src/styles/menu/SideMenu.styles.js
@@ -0,0 +1,66 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили компонента бокового меню
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet, Platform } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили бокового меню
+const styles = StyleSheet.create({
+ modalContainer: {
+ flex: 1,
+ flexDirection: 'row',
+ justifyContent: 'flex-end'
+ },
+ backdrop: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: APP_COLORS.overlay
+ },
+ backdropPressable: {
+ flex: 1
+ },
+ menuContainer: {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: APP_COLORS.surface,
+ ...Platform.select({
+ ios: {
+ shadowColor: APP_COLORS.black,
+ shadowOpacity: 0.25,
+ shadowRadius: 10,
+ shadowOffset: {
+ width: -3,
+ height: 0
+ }
+ },
+ android: {
+ elevation: 24
+ },
+ web: {
+ boxShadow: '-3px 0px 10px rgba(0, 0, 0, 0.25)'
+ }
+ })
+ },
+ header: {
+ borderBottomWidth: 1,
+ borderBottomColor: APP_COLORS.borderSubtle,
+ backgroundColor: APP_COLORS.primaryLight
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/screens/MainScreen.styles.js b/rn/app/src/styles/screens/MainScreen.styles.js
new file mode 100644
index 0000000..82d4514
--- /dev/null
+++ b/rn/app/src/styles/screens/MainScreen.styles.js
@@ -0,0 +1,48 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили главного экрана приложения
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { StyleSheet } = require('react-native'); //StyleSheet React Native
+const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
+const { UI } = require('../../config/appConfig'); //Конфигурация UI
+const { responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
+
+//-----------
+//Тело модуля
+//-----------
+
+//Стили главного экрана
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: APP_COLORS.background
+ },
+ content: {
+ flex: 1,
+ paddingTop: responsiveSpacing(2)
+ },
+ emptyState: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: UI.PADDING
+ },
+ emptyStateText: {
+ textAlign: 'center',
+ marginBottom: responsiveSpacing(4)
+ },
+ emptyStateButton: {
+ minWidth: responsiveSpacing(40)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = styles;
diff --git a/rn/app/src/styles/screens/SettingsScreen.styles.js b/rn/app/src/styles/screens/SettingsScreen.styles.js
new file mode 100644
index 0000000..4648bb1
--- /dev/null
+++ b/rn/app/src/styles/screens/SettingsScreen.styles.js
@@ -0,0 +1,127 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Стили экрана настроек
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+const { StyleSheet } = require('react-native');
+const { APP_COLORS } = require('../../config/theme');
+const { UI } = require('../../config/appConfig');
+const { responsiveSpacing } = require('../../utils/responsive');
+
+//-----------
+//Тело модуля
+//-----------
+
+const styles = StyleSheet.create({
+ scrollView: {
+ flex: 1
+ },
+ scrollContent: {
+ paddingBottom: responsiveSpacing(8)
+ },
+ section: {
+ backgroundColor: APP_COLORS.surface,
+ marginTop: responsiveSpacing(4),
+ marginHorizontal: UI.PADDING,
+ paddingHorizontal: UI.PADDING,
+ paddingVertical: responsiveSpacing(4),
+ borderRadius: UI.BORDER_RADIUS,
+ borderWidth: 1,
+ borderColor: APP_COLORS.borderSubtle
+ },
+ sectionTitle: {
+ marginBottom: responsiveSpacing(3)
+ },
+ fieldLabel: {
+ marginBottom: responsiveSpacing(2),
+ color: APP_COLORS.textSecondary
+ },
+ serverUrlRow: {
+ flexDirection: 'row',
+ alignItems: 'center'
+ },
+ serverUrlField: {
+ flex: 1,
+ height: UI.INPUT_HEIGHT,
+ borderWidth: 1,
+ borderColor: APP_COLORS.borderMedium,
+ borderRadius: UI.BORDER_RADIUS,
+ paddingHorizontal: responsiveSpacing(3),
+ justifyContent: 'center',
+ backgroundColor: APP_COLORS.surface
+ },
+ serverUrlFieldPressed: {
+ borderColor: APP_COLORS.primary,
+ backgroundColor: APP_COLORS.primaryExtraLight
+ },
+ serverUrlText: {
+ fontSize: UI.FONT_SIZE_MD,
+ color: APP_COLORS.textPrimary
+ },
+ serverUrlPlaceholder: {
+ color: APP_COLORS.textTertiary
+ },
+ serverUrlCopyButton: {
+ marginLeft: responsiveSpacing(2)
+ },
+ actionButton: {
+ marginTop: responsiveSpacing(3)
+ },
+ clearCacheButton: {
+ backgroundColor: APP_COLORS.error
+ },
+ clearCacheButtonText: {
+ color: APP_COLORS.white
+ },
+ resetButton: {
+ backgroundColor: APP_COLORS.warning
+ },
+ resetButtonText: {
+ color: APP_COLORS.white
+ },
+ optimizeButton: {
+ backgroundColor: APP_COLORS.info
+ },
+ optimizeButtonText: {
+ color: APP_COLORS.white
+ },
+ infoButton: {
+ backgroundColor: APP_COLORS.primaryLight
+ },
+ infoRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: responsiveSpacing(2.5),
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: APP_COLORS.borderSubtle
+ },
+ infoLabel: {
+ flex: 1
+ },
+ infoValue: {
+ flex: 2,
+ textAlign: 'right'
+ },
+ infoValueWithAction: {
+ flex: 2,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'flex-end'
+ },
+ infoValueFlex: {
+ flex: 1,
+ textAlign: 'right'
+ },
+ copyButton: {
+ marginLeft: responsiveSpacing(2)
+ }
+});
+
+//----------------
+//Интерфейс модуля
+//----------------
+module.exports = styles;
diff --git a/rn/app/src/utils/appInfo.js b/rn/app/src/utils/appInfo.js
new file mode 100644
index 0000000..8da97c5
--- /dev/null
+++ b/rn/app/src/utils/appInfo.js
@@ -0,0 +1,87 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Утилиты информации о приложении и режимах работы
+*/
+
+//---------
+//Константы
+//---------
+
+//Версия приложения
+const APP_VERSION = '1.0.0';
+
+//Название приложения
+const APP_NAME = 'Парус© Предрейсовые осмотры';
+
+//Описание приложения
+const APP_DESCRIPTION = 'Предназначено для проведения предрейсовых осмотров транспортных средств.';
+
+//Режимы работы на русском
+const MODE_LABELS = {
+ ONLINE: 'Онлайн',
+ OFFLINE: 'Офлайн',
+ NOT_CONNECTED: 'Не подключено'
+};
+
+//Описания режимов работы
+const MODE_DESCRIPTIONS = {
+ ONLINE: 'Приложение подключено к серверу и синхронизирует данные.',
+ OFFLINE: 'Приложение работает без подключения к серверу. Используются локальные данные.',
+ NOT_CONNECTED: 'Приложение ещё не подключалось к серверу. Настройте сервер в настройках.'
+};
+
+//-----------
+//Тело модуля
+//-----------
+
+//Получение названия режима на русском
+const getModeLabel = mode => {
+ return MODE_LABELS[mode] || 'Неизвестно';
+};
+
+//Получение описания режима
+const getModeDescription = mode => {
+ return MODE_DESCRIPTIONS[mode] || 'Неизвестный режим работы.';
+};
+
+//Формирование информации о приложении
+const getAppInfo = (options = {}) => {
+ const { mode, serverUrl, isDbReady } = options;
+
+ const lines = [`Версия: ${APP_VERSION}`];
+
+ if (mode) {
+ lines.push(`Режим работы: ${getModeLabel(mode)}`);
+ }
+
+ if (serverUrl !== undefined) {
+ lines.push(`Сервер: ${serverUrl || 'не настроен'}`);
+ }
+
+ if (isDbReady !== undefined) {
+ lines.push(`База данных: ${isDbReady ? 'готова' : 'загружается...'}`);
+ }
+
+ return lines.join('\n');
+};
+
+//Формирование полного описания приложения
+const getFullAppDescription = () => {
+ return `Демонстрационное приложение\nВерсия ${APP_VERSION}\n\n${APP_DESCRIPTION}`;
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ APP_VERSION,
+ APP_NAME,
+ APP_DESCRIPTION,
+ MODE_LABELS,
+ MODE_DESCRIPTIONS,
+ getModeLabel,
+ getModeDescription,
+ getAppInfo,
+ getFullAppDescription
+};
diff --git a/rn/app/src/utils/clipboard.js b/rn/app/src/utils/clipboard.js
new file mode 100644
index 0000000..bf73329
--- /dev/null
+++ b/rn/app/src/utils/clipboard.js
@@ -0,0 +1,107 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Утилита для работы с буфером обмена
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { Platform, NativeModules } = require('react-native'); //React Native
+
+//-----------
+//Тело модуля
+//-----------
+
+//Получение нативного модуля буфера обмена
+const getNativeClipboard = () => {
+ //Android: ClipboardModule
+ if (Platform.OS === 'android') {
+ return NativeModules.ClipboardModule || NativeModules.Clipboard;
+ }
+
+ //iOS: RCTClipboard
+ if (Platform.OS === 'ios') {
+ return NativeModules.RCTClipboard || NativeModules.Clipboard;
+ }
+
+ return null;
+};
+
+//Копирование текста в буфер обмена
+const copyToClipboard = async text => {
+ const value = String(text || '');
+
+ //Web: используем navigator.clipboard
+ if (Platform.OS === 'web') {
+ if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
+ await navigator.clipboard.writeText(value);
+ return true;
+ }
+
+ //Fallback для старых браузеров
+ if (typeof document !== 'undefined') {
+ const textArea = document.createElement('textarea');
+ textArea.value = value;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-9999px';
+ document.body.appendChild(textArea);
+ textArea.select();
+ const success = document.execCommand('copy');
+ document.body.removeChild(textArea);
+ return success;
+ }
+
+ throw new Error('Буфер обмена недоступен');
+ }
+
+ //Android/iOS: используем нативный модуль
+ const nativeClipboard = getNativeClipboard();
+
+ if (nativeClipboard) {
+ //Пробуем разные методы
+ if (typeof nativeClipboard.setString === 'function') {
+ await nativeClipboard.setString(value);
+ return true;
+ }
+
+ if (typeof nativeClipboard.setContent === 'function') {
+ await nativeClipboard.setContent({ text: value });
+ return true;
+ }
+ }
+
+ throw new Error('Нативный модуль буфера обмена недоступен');
+};
+
+//Чтение текста из буфера обмена
+const readFromClipboard = async () => {
+ //Web: используем navigator.clipboard
+ if (Platform.OS === 'web') {
+ if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.readText === 'function') {
+ return await navigator.clipboard.readText();
+ }
+
+ throw new Error('Буфер обмена недоступен');
+ }
+
+ //Android/iOS: используем нативный модуль
+ const nativeClipboard = getNativeClipboard();
+
+ if (nativeClipboard) {
+ if (typeof nativeClipboard.getString === 'function') {
+ return await nativeClipboard.getString();
+ }
+ }
+
+ throw new Error('Нативный модуль буфера обмена недоступен');
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ copyToClipboard,
+ readFromClipboard
+};
diff --git a/rn/app/src/utils/responsive.js b/rn/app/src/utils/responsive.js
new file mode 100644
index 0000000..e9b9da9
--- /dev/null
+++ b/rn/app/src/utils/responsive.js
@@ -0,0 +1,127 @@
+/*
+ Предрейсовые осмотры - мобильное приложение
+ Утилиты для адаптивного дизайна
+*/
+
+//---------------------
+//Подключение библиотек
+//---------------------
+
+const { Dimensions, Platform, PixelRatio } = require('react-native'); //Размеры экрана и платформа
+
+//-----------
+//Тело модуля
+//-----------
+
+//Константы для базовых размеров
+const BASE_WIDTH = 375; // iPhone 11/12/13/14
+const BASE_HEIGHT = 812;
+
+//Получение размеров экрана
+const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
+
+//Проверка на старые устройства Android
+const isLegacyAndroid = () => {
+ return Platform.OS === 'android' && Platform.Version < 26; // Android 8.0 = API 26
+};
+
+//Нормализация размера для разных плотностей пикселей
+const normalize = (size, based = 'width') => {
+ const scale = based === 'height' ? SCREEN_HEIGHT / BASE_HEIGHT : SCREEN_WIDTH / BASE_WIDTH;
+
+ const newSize = size * scale;
+
+ //Для старых Android используем более простую нормализацию
+ if (isLegacyAndroid()) {
+ return Math.round(newSize);
+ }
+
+ if (Platform.OS === 'ios') {
+ return Math.round(PixelRatio.roundToNearestPixel(newSize));
+ } else {
+ return Math.round(PixelRatio.roundToNearestPixel(newSize)) - 2;
+ }
+};
+
+//Адаптивная ширина (по ширине экрана)
+const responsiveWidth = size => normalize(size, 'width');
+
+//Адаптивная высота (по высоте экрана)
+const responsiveHeight = size => normalize(size, 'height');
+
+//Адаптивный размер (интеллектуальное масштабирование)
+const responsiveSize = size => {
+ const widthScale = SCREEN_WIDTH / BASE_WIDTH;
+ const heightScale = SCREEN_HEIGHT / BASE_HEIGHT;
+ const scale = Math.min(widthScale, heightScale);
+
+ const newSize = size * scale;
+
+ //Для старых Android используем более простую нормализацию
+ if (isLegacyAndroid()) {
+ return Math.round(newSize);
+ }
+
+ if (Platform.OS === 'ios') {
+ return Math.round(PixelRatio.roundToNearestPixel(newSize));
+ } else {
+ return Math.round(PixelRatio.roundToNearestPixel(newSize));
+ }
+};
+
+//Процент от ширины экрана
+const widthPercentage = percent => {
+ return Math.round((SCREEN_WIDTH * percent) / 100);
+};
+
+//Процент от высоты экрана
+const heightPercentage = percent => {
+ return Math.round((SCREEN_HEIGHT * percent) / 100);
+};
+
+//Проверка на планшет
+const isTablet = () => {
+ const aspectRatio = SCREEN_WIDTH / SCREEN_HEIGHT;
+ return (
+ (Platform.OS === 'ios' && Platform.isPad) ||
+ (SCREEN_WIDTH >= 768 && SCREEN_HEIGHT >= 1024) ||
+ aspectRatio <= 0.6 ||
+ (SCREEN_WIDTH > 600 && SCREEN_HEIGHT > 600)
+ );
+};
+
+//Проверка на маленький экран (меньше iPhone SE)
+const isSmallScreen = () => {
+ return SCREEN_WIDTH < 375;
+};
+
+//Проверка на большой экран (больше планшетов)
+const isLargeScreen = () => {
+ return SCREEN_WIDTH > 768;
+};
+
+//Адаптивный отступ
+const responsiveSpacing = (size = 1) => {
+ const baseSpacing = 4; // 4px базовый отступ
+ return responsiveSize(baseSpacing * size);
+};
+
+//----------------
+//Интерфейс модуля
+//----------------
+
+module.exports = {
+ responsiveWidth,
+ responsiveHeight,
+ responsiveSize,
+ responsiveSpacing,
+ widthPercentage,
+ heightPercentage,
+ isTablet,
+ isSmallScreen,
+ isLargeScreen,
+ isLegacyAndroid,
+ SCREEN_WIDTH,
+ SCREEN_HEIGHT,
+ normalize
+};
diff --git a/rn/app/tsconfig.json b/rn/app/tsconfig.json
new file mode 100644
index 0000000..790ac61
--- /dev/null
+++ b/rn/app/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "jsx": "react-native",
+ "noEmit": true,
+ "types": ["jest"]
+ },
+ "include": ["**/*"],
+ "exclude": ["**/node_modules", "**/Pods"],
+}