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) => ( + + handleItemPress(item)} + isDestructive={item.isDestructive} + disabled={item.disabled} + style={item.style} + textStyle={item.textStyle} + /> + {item.showDivider && index < items.length - 1 && } + + ))} + + ); +} + +//---------------- +//Интерфейс модуля +//---------------- + +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"], +}