Добавлен экран аутентификации, общеиспользуемые константы вынесены в отдельные модули
This commit is contained in:
parent
561763dc74
commit
5da91dacbc
341
rn/app/scripts/generate-app-icons.js
Normal file
341
rn/app/scripts/generate-app-icons.js
Normal file
@ -0,0 +1,341 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Генератор иконок приложения из одного PNG
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
const APP_ROOT = path.join(__dirname, '..');
|
||||
const LOGO_PATH = path.join(APP_ROOT, 'assets', 'icons', 'logo.png');
|
||||
|
||||
const ANDROID_SIZES = Object.freeze([
|
||||
Object.freeze({ dir: 'mipmap-mdpi', size: 48 }),
|
||||
Object.freeze({ dir: 'mipmap-hdpi', size: 72 }),
|
||||
Object.freeze({ dir: 'mipmap-xhdpi', size: 96 }),
|
||||
Object.freeze({ dir: 'mipmap-xxhdpi', size: 144 }),
|
||||
Object.freeze({ dir: 'mipmap-xxxhdpi', size: 192 })
|
||||
]);
|
||||
|
||||
const IOS_ICONS = Object.freeze([
|
||||
Object.freeze({ filename: 'icon-20@2x.png', size: 40 }),
|
||||
Object.freeze({ filename: 'icon-20@3x.png', size: 60 }),
|
||||
Object.freeze({ filename: 'icon-29@2x.png', size: 58 }),
|
||||
Object.freeze({ filename: 'icon-29@3x.png', size: 87 }),
|
||||
Object.freeze({ filename: 'icon-40@2x.png', size: 80 }),
|
||||
Object.freeze({ filename: 'icon-40@3x.png', size: 120 }),
|
||||
Object.freeze({ filename: 'icon-60@2x.png', size: 120 }),
|
||||
Object.freeze({ filename: 'icon-60@3x.png', size: 180 }),
|
||||
Object.freeze({ filename: 'icon-1024.png', size: 1024 })
|
||||
]);
|
||||
|
||||
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ANDROID_RES_DIR = path.join(APP_ROOT, 'android', 'app', 'src', 'main', 'res');
|
||||
const IOS_APP_ICON_DIR = path.join(APP_ROOT, 'ios', 'app', 'Images.xcassets', 'AppIcon.appiconset');
|
||||
|
||||
const U32_MAX = 4294967296;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Приведение числа к беззнаковому 32-битному (без побитовых операторов)
|
||||
function u32(x) {
|
||||
return ((x % U32_MAX) + U32_MAX) % U32_MAX;
|
||||
}
|
||||
|
||||
//Побитовое XOR для двух 32-битных беззнаковых (через арифметику, для соответствия ESLint)
|
||||
function xor32(a, b) {
|
||||
const au = u32(a);
|
||||
const bu = u32(b);
|
||||
let r = 0;
|
||||
let p = 1;
|
||||
for (let i = 0; i < 32; i++) {
|
||||
if (Math.floor(au / p) % 2 !== Math.floor(bu / p) % 2) r += p;
|
||||
p *= 2;
|
||||
}
|
||||
return u32(r);
|
||||
}
|
||||
|
||||
//Байт в диапазоне 0..255 (эквивалент & 0xff для суммы/разности байтов)
|
||||
function byteClamp(v) {
|
||||
return ((v % 256) + 256) % 256;
|
||||
}
|
||||
|
||||
//Таблица CRC32 для PNG-чанков (полином IEEE, без побитовых операторов)
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
const poly = 0xedb88320;
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const low = c % 2;
|
||||
c = Math.floor(u32(c) / 2);
|
||||
if (low) c = xor32(poly, c);
|
||||
}
|
||||
table[n] = u32(c);
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
//CRC32 от буфера (тип чанка + данные), для проверки и записи PNG. Индекс таблицы — младший байт (crc XOR byte).
|
||||
function crc32(buffer, start = 0, length = buffer.length - start) {
|
||||
let crc = 0xffffffff;
|
||||
const end = start + length;
|
||||
for (let i = start; i < end; i++) {
|
||||
const lo = xor32(crc, buffer[i]);
|
||||
const indexByte = u32(lo) % 256;
|
||||
crc = xor32(CRC_TABLE[indexByte], Math.floor(u32(crc) / 256));
|
||||
}
|
||||
return u32(xor32(crc, 0xffffffff));
|
||||
}
|
||||
|
||||
//Чтение PNG: файл → RGBA (Uint8Array, по строкам, 4 байта на пиксель). Поддержка RGB/RGBA 8-bit.
|
||||
function readPng(filePath) {
|
||||
const data = fs.readFileSync(filePath);
|
||||
let offset = 0;
|
||||
|
||||
if (data.length < 8 || !PNG_SIGNATURE.equals(data.subarray(0, 8))) {
|
||||
throw new Error(`Invalid PNG signature: ${filePath}`);
|
||||
}
|
||||
offset = 8;
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let bitDepth = 0;
|
||||
let colorType = 0;
|
||||
let interlace = 0;
|
||||
const idatChunks = [];
|
||||
|
||||
while (offset < data.length) {
|
||||
const length = data.readUInt32BE(offset);
|
||||
const type = data.toString('ascii', offset + 4, offset + 8);
|
||||
const chunkStart = offset + 8;
|
||||
const chunkEnd = chunkStart + length;
|
||||
|
||||
if (chunkEnd + 4 > data.length) throw new Error('PNG truncated');
|
||||
const crcExpected = data.readUInt32BE(chunkEnd);
|
||||
const typeAndDataStart = offset + 4;
|
||||
const crcInput = data.subarray(typeAndDataStart, typeAndDataStart + 4 + length);
|
||||
const crcGot = crc32(crcInput);
|
||||
if (crcGot !== crcExpected) throw new Error(`PNG chunk CRC mismatch: ${type}`);
|
||||
|
||||
offset = chunkEnd + 4;
|
||||
|
||||
if (type === 'IHDR') {
|
||||
if (length < 13) throw new Error('IHDR too short');
|
||||
width = data.readUInt32BE(chunkStart);
|
||||
height = data.readUInt32BE(chunkStart + 4);
|
||||
bitDepth = data[chunkStart + 8];
|
||||
colorType = data[chunkStart + 9];
|
||||
interlace = data[chunkStart + 12];
|
||||
} else if (type === 'IDAT') {
|
||||
idatChunks.push(data.subarray(chunkStart, chunkEnd));
|
||||
} else if (type === 'IEND') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (interlace !== 0) throw new Error('Interlaced PNG is not supported');
|
||||
if (bitDepth !== 8) throw new Error('Only 8-bit PNG is supported');
|
||||
if (colorType !== 2 && colorType !== 6) {
|
||||
throw new Error('Only RGB or RGBA PNG is supported (color type 2 or 6)');
|
||||
}
|
||||
|
||||
const bytesPerPixel = colorType === 6 ? 4 : 3;
|
||||
const rawSize = (1 + width * bytesPerPixel) * height;
|
||||
const concatenated = Buffer.concat(idatChunks);
|
||||
const inflated = zlib.inflateSync(concatenated, { chunkSize: rawSize + 1024 });
|
||||
if (inflated.length < rawSize) throw new Error('PNG decompressed size too small');
|
||||
|
||||
// Снятие фильтров строк и приведение к RGBA
|
||||
const rgba = new Uint8Array(width * height * 4);
|
||||
const stride = width * bytesPerPixel;
|
||||
let prev = null;
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
const rowStart = y * (1 + stride);
|
||||
const filter = inflated[rowStart];
|
||||
const raw = inflated.subarray(rowStart + 1, rowStart + 1 + stride);
|
||||
const out = new Uint8Array(stride);
|
||||
|
||||
for (let x = 0; x < stride; x++) {
|
||||
const left = x >= bytesPerPixel ? out[x - bytesPerPixel] : 0;
|
||||
const up = prev ? prev[x] : 0;
|
||||
const upLeft = prev && x >= bytesPerPixel ? prev[x - bytesPerPixel] : 0;
|
||||
|
||||
let v = raw[x];
|
||||
if (filter === 1) v = byteClamp(v + left);
|
||||
else if (filter === 2) v = byteClamp(v + up);
|
||||
else if (filter === 3) v = byteClamp(v + Math.floor((left + up) / 2));
|
||||
else if (filter === 4) v = byteClamp(v + paeth(left, up, upLeft));
|
||||
out[x] = v;
|
||||
}
|
||||
prev = out;
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = x * bytesPerPixel;
|
||||
const o = (y * width + x) * 4;
|
||||
rgba[o] = out[i];
|
||||
rgba[o + 1] = out[i + 1];
|
||||
rgba[o + 2] = out[i + 2];
|
||||
rgba[o + 3] = bytesPerPixel === 4 ? out[i + 3] : 255;
|
||||
}
|
||||
}
|
||||
|
||||
return { width, height, rgba };
|
||||
}
|
||||
|
||||
//Предиктор Paeth для фильтра PNG (без побитовых операций)
|
||||
function paeth(a, b, c) {
|
||||
const p = a + b - c;
|
||||
const pa = Math.abs(p - a);
|
||||
const pb = Math.abs(p - b);
|
||||
const pc = Math.abs(p - c);
|
||||
if (pa <= pb && pa <= pc) return a;
|
||||
if (pb <= pc) return b;
|
||||
return c;
|
||||
}
|
||||
|
||||
//Масштабирование RGBA билинейной интерполяцией
|
||||
function resizeBilinear(srcWidth, srcHeight, srcRgba, dstWidth, dstHeight) {
|
||||
const dst = new Uint8Array(dstWidth * dstHeight * 4);
|
||||
const src = srcRgba;
|
||||
|
||||
for (let dy = 0; dy < dstHeight; dy++) {
|
||||
const sy = (dy + 0.5) * (srcHeight / dstHeight) - 0.5;
|
||||
const sy0 = Math.max(0, Math.floor(sy));
|
||||
const sy1 = Math.min(srcHeight - 1, sy0 + 1);
|
||||
const fy = sy - sy0;
|
||||
|
||||
for (let dx = 0; dx < dstWidth; dx++) {
|
||||
const sx = (dx + 0.5) * (srcWidth / dstWidth) - 0.5;
|
||||
const sx0 = Math.max(0, Math.floor(sx));
|
||||
const sx1 = Math.min(srcWidth - 1, sx0 + 1);
|
||||
const fx = sx - sx0;
|
||||
|
||||
const i00 = (sy0 * srcWidth + sx0) * 4;
|
||||
const i10 = (sy0 * srcWidth + sx1) * 4;
|
||||
const i01 = (sy1 * srcWidth + sx0) * 4;
|
||||
const i11 = (sy1 * srcWidth + sx1) * 4;
|
||||
|
||||
const o = (dy * dstWidth + dx) * 4;
|
||||
for (let ch = 0; ch < 4; ch++) {
|
||||
const v00 = src[i00 + ch];
|
||||
const v10 = src[i10 + ch];
|
||||
const v01 = src[i01 + ch];
|
||||
const v11 = src[i11 + ch];
|
||||
const top = v00 * (1 - fx) + v10 * fx;
|
||||
const bot = v01 * (1 - fx) + v11 * fx;
|
||||
dst[o + ch] = Math.round(top * (1 - fy) + bot * fy);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
//Кодирование RGBA в буфер PNG (фильтр None, deflate)
|
||||
function writePngBuffer(width, height, rgba) {
|
||||
const stride = width * 4;
|
||||
const rawRows = [];
|
||||
for (let y = 0; y < height; y++) {
|
||||
const row = Buffer.alloc(1 + stride);
|
||||
row[0] = 0; // фильтр None
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
row[1 + x * 4] = rgba[i];
|
||||
row[2 + x * 4] = rgba[i + 1];
|
||||
row[3 + x * 4] = rgba[i + 2];
|
||||
row[4 + x * 4] = rgba[i + 3];
|
||||
}
|
||||
rawRows.push(row);
|
||||
}
|
||||
const rawData = Buffer.concat(rawRows);
|
||||
const compressed = zlib.deflateSync(rawData, { level: 9 });
|
||||
|
||||
function writeChunk(out, type, data) {
|
||||
const len = data ? data.length : 0;
|
||||
const buf = Buffer.alloc(12 + len);
|
||||
buf.writeUInt32BE(len, 0);
|
||||
buf.write(type, 4, 4, 'ascii');
|
||||
if (data) data.copy(buf, 8);
|
||||
const crc = crc32(Buffer.concat([Buffer.from(type, 'ascii'), data || Buffer.alloc(0)]));
|
||||
buf.writeUInt32BE(crc, 8 + len);
|
||||
out.push(buf);
|
||||
}
|
||||
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // глубина цвета
|
||||
ihdr[9] = 6; // тип цвета RGBA
|
||||
ihdr[10] = 0; // сжатие deflate
|
||||
ihdr[11] = 0; // фильтр адаптивный
|
||||
ihdr[12] = 0; // без чересстрочности
|
||||
|
||||
const out = [PNG_SIGNATURE];
|
||||
writeChunk(out, 'IHDR', ihdr);
|
||||
writeChunk(out, 'IDAT', compressed);
|
||||
writeChunk(out, 'IEND', null);
|
||||
return Buffer.concat(out);
|
||||
}
|
||||
|
||||
//Создание каталога при необходимости
|
||||
function ensureDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
//Генерация всех иконок: один раз читаем логотип, ресайзим и пишем в Android/iOS
|
||||
async function generate() {
|
||||
if (!fs.existsSync(LOGO_PATH)) {
|
||||
const err = new Error(`Logo not found: ${LOGO_PATH}`);
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const logo = readPng(LOGO_PATH);
|
||||
const { width: w, height: h, rgba } = logo;
|
||||
|
||||
for (const { dir, size } of ANDROID_SIZES) {
|
||||
const dirPath = path.join(ANDROID_RES_DIR, dir);
|
||||
ensureDir(dirPath);
|
||||
const resized = resizeBilinear(w, h, rgba, size, size);
|
||||
const png = writePngBuffer(size, size, resized);
|
||||
const base = path.join(dirPath, 'ic_launcher.png');
|
||||
const round = path.join(dirPath, 'ic_launcher_round.png');
|
||||
await Promise.all([fs.promises.writeFile(base, png), fs.promises.writeFile(round, png)]);
|
||||
console.log('Android', dir, `${size}x${size}`);
|
||||
}
|
||||
|
||||
for (const { filename, size } of IOS_ICONS) {
|
||||
const outPath = path.join(IOS_APP_ICON_DIR, filename);
|
||||
const resized = resizeBilinear(w, h, rgba, size, size);
|
||||
const png = writePngBuffer(size, size, resized);
|
||||
await fs.promises.writeFile(outPath, png);
|
||||
console.log('iOS', filename, `${size}x${size}`);
|
||||
}
|
||||
|
||||
console.log('Done. App icon set from', LOGO_PATH);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Точка входа
|
||||
//----------------
|
||||
|
||||
generate().catch(err => {
|
||||
console.error(err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
158
rn/app/src/components/auth/OrganizationSelectDialog.js
Normal file
158
rn/app/src/components/auth/OrganizationSelectDialog.js
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Диалог выбора организации
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { Modal, View, FlatList, Pressable, Keyboard } = require('react-native');
|
||||
const { useSafeAreaInsets } = require('react-native-safe-area-context');
|
||||
const AppText = require('../common/AppText');
|
||||
const AppButton = require('../common/AppButton');
|
||||
const styles = require('../../styles/auth/OrganizationSelectDialog.styles');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Иконка закрытия (крестик)
|
||||
function CloseIcon() {
|
||||
return (
|
||||
<View style={styles.closeIcon}>
|
||||
<View style={styles.closeIconLine1} />
|
||||
<View style={styles.closeIconLine2} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Стиль элемента организации
|
||||
function getOrganizationItemStyle(stylesRef, isSelected, pressed) {
|
||||
return [stylesRef.organizationItem, isSelected && stylesRef.organizationItemSelected, pressed && stylesRef.organizationItemPressed];
|
||||
}
|
||||
|
||||
//Элемент списка организаций
|
||||
function OrganizationItem({ item, onSelect, isSelected }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect(item);
|
||||
}
|
||||
}, [item, onSelect]);
|
||||
|
||||
const styleFn = React.useCallback(({ pressed }) => getOrganizationItemStyle(styles, isSelected, pressed), [isSelected]);
|
||||
|
||||
return (
|
||||
<Pressable style={styleFn} onPress={handlePress} delayPressIn={0}>
|
||||
<AppText style={[styles.organizationName, isSelected && styles.organizationNameSelected]} numberOfLines={2}>
|
||||
{item.SNAME}
|
||||
</AppText>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//Диалог выбора организации
|
||||
function OrganizationSelectDialog({ visible, organizations = [], onSelect, onCancel, title = 'Выбор организации' }) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [selectedOrg, setSelectedOrg] = React.useState(null);
|
||||
|
||||
//При открытии диалога: сброс выбора и снятие фокуса с клавиатуры
|
||||
React.useEffect(() => {
|
||||
if (visible) {
|
||||
setSelectedOrg(null);
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
//Обработчик выбора организации из списка
|
||||
const handleSelectItem = React.useCallback(org => {
|
||||
setSelectedOrg(org);
|
||||
}, []);
|
||||
|
||||
//Обработчик подтверждения выбора
|
||||
const handleConfirm = React.useCallback(() => {
|
||||
if (selectedOrg && typeof onSelect === 'function') {
|
||||
onSelect(selectedOrg);
|
||||
}
|
||||
}, [selectedOrg, onSelect]);
|
||||
|
||||
//Обработчик отмены
|
||||
const handleCancel = React.useCallback(() => {
|
||||
if (typeof onCancel === 'function') {
|
||||
onCancel();
|
||||
}
|
||||
}, [onCancel]);
|
||||
|
||||
//Функция отрисовки элемента списка
|
||||
const renderItem = React.useCallback(
|
||||
({ item }) => {
|
||||
const isSelected = selectedOrg != null && item != null && String(selectedOrg.NRN) === String(item.NRN);
|
||||
return <OrganizationItem item={item} onSelect={handleSelectItem} isSelected={!!isSelected} />;
|
||||
},
|
||||
[selectedOrg, handleSelectItem]
|
||||
);
|
||||
|
||||
//Генератор ключей для списка
|
||||
const keyExtractor = React.useCallback(item => String(item.NRN), []);
|
||||
|
||||
//Стиль кнопки закрытия диалога при нажатии
|
||||
const getDialogCloseButtonPressableStyle = React.useMemo(() => {
|
||||
return function styleFn({ pressed }) {
|
||||
return [styles.closeButton, pressed && styles.closeButtonPressed];
|
||||
};
|
||||
}, []);
|
||||
|
||||
//Стили с учетом safe area
|
||||
const footerStyle = React.useMemo(
|
||||
() => [styles.footer, { paddingBottom: Math.max(insets.bottom, styles.footer.padding || 16) }],
|
||||
[insets.bottom]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent={true} animationType="fade" statusBarTranslucent={true} onRequestClose={handleCancel}>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.dialog}>
|
||||
<View style={styles.header}>
|
||||
<AppText style={styles.title} variant="h3" weight="semibold">
|
||||
{title}
|
||||
</AppText>
|
||||
|
||||
<Pressable style={getDialogCloseButtonPressableStyle} onPress={handleCancel} hitSlop={8}>
|
||||
<CloseIcon />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={organizations}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
extraData={selectedOrg}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={true}
|
||||
contentContainerStyle={styles.listContent}
|
||||
style={styles.list}
|
||||
/>
|
||||
|
||||
<View style={footerStyle}>
|
||||
<AppButton title="Отмена" onPress={handleCancel} style={styles.cancelButton} textStyle={styles.cancelButtonText} />
|
||||
|
||||
<AppButton
|
||||
title="Выбрать"
|
||||
onPress={handleConfirm}
|
||||
disabled={!selectedOrg}
|
||||
style={styles.confirmButton}
|
||||
textStyle={styles.confirmButtonText}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = OrganizationSelectDialog;
|
||||
@ -16,18 +16,23 @@ const styles = require('../../styles/common/AppButton.styles'); //Стили к
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Стиль кнопки в зависимости от состояния нажатия
|
||||
function getAppButtonPressableStyle(stylesRef, disabled, style) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [stylesRef.base, disabled && stylesRef.disabled, pressed && !disabled && stylesRef.pressed, style];
|
||||
};
|
||||
}
|
||||
|
||||
//Общая кнопка приложения
|
||||
function AppButton({ title, onPress, disabled = false, style, textStyle }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
if (!disabled && typeof onPress === 'function') onPress();
|
||||
}, [disabled, onPress]);
|
||||
|
||||
const getPressableStyle = React.useMemo(() => getAppButtonPressableStyle(styles, disabled, style), [disabled, style]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
disabled={disabled}
|
||||
style={({ pressed }) => [styles.base, disabled && styles.disabled, pressed && !disabled && styles.pressed, style]}
|
||||
>
|
||||
<Pressable onPress={handlePress} disabled={disabled} style={getPressableStyle}>
|
||||
<View style={styles.content}>
|
||||
<AppText style={[styles.text, textStyle]}>{title}</AppText>
|
||||
</View>
|
||||
|
||||
@ -16,37 +16,48 @@ 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
|
||||
}) {
|
||||
//Адаптивный компонент ввода (с поддержкой ref для фокуса)
|
||||
const AppInput = React.forwardRef(function AppInput(
|
||||
{
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
secureTextEntry = false,
|
||||
keyboardType = 'default',
|
||||
autoCapitalize = 'none',
|
||||
error,
|
||||
helperText,
|
||||
disabled = false,
|
||||
style,
|
||||
inputStyle,
|
||||
labelStyle,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
const handleFocus = () => setIsFocused(true);
|
||||
const handleBlur = () => setIsFocused(false);
|
||||
//Обработчик фокуса
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
//Обработчик потери фокуса
|
||||
const handleBlur = React.useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{label && (
|
||||
{label ? (
|
||||
<AppText style={[styles.label, labelStyle]} variant="caption" weight="medium">
|
||||
{label}
|
||||
</AppText>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<TextInput
|
||||
ref={ref}
|
||||
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError, disabled && styles.inputDisabled, inputStyle]}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
@ -55,6 +66,7 @@ function AppInput({
|
||||
secureTextEntry={secureTextEntry}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
autoFocus={false}
|
||||
editable={!disabled}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@ -64,14 +76,14 @@ function AppInput({
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
{(error || helperText) && (
|
||||
{error || helperText ? (
|
||||
<AppText style={[styles.helperText, error && styles.helperTextError]} variant="caption">
|
||||
{error || helperText}
|
||||
</AppText>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
|
||||
@ -8,59 +8,43 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const { View, Image } = require('react-native'); //Базовые компоненты
|
||||
const { APP_LOGO, LOGO_SIZE_KEYS } = require('../../config/appAssets'); //Ресурсы приложения
|
||||
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]);
|
||||
//Возвращает стили контейнера в зависимости от размера
|
||||
function getContainerStyleBySize(size) {
|
||||
switch (size) {
|
||||
case LOGO_SIZE_KEYS.SMALL:
|
||||
return styles.containerSmall;
|
||||
case LOGO_SIZE_KEYS.LARGE:
|
||||
return styles.containerLarge;
|
||||
case LOGO_SIZE_KEYS.MEDIUM:
|
||||
default:
|
||||
return styles.containerMedium;
|
||||
}
|
||||
}
|
||||
|
||||
const sizeStyles = getSizeStyles();
|
||||
//Нормализация пропа size к допустимому значению
|
||||
function normalizeSize(size) {
|
||||
if (size === LOGO_SIZE_KEYS.SMALL || size === LOGO_SIZE_KEYS.LARGE) {
|
||||
return size;
|
||||
}
|
||||
return LOGO_SIZE_KEYS.MEDIUM;
|
||||
}
|
||||
|
||||
//Иконка/логотип приложения
|
||||
function AppLogo({ size = LOGO_SIZE_KEYS.MEDIUM, style }) {
|
||||
const normalizedSize = normalizeSize(size);
|
||||
const containerSizeStyle = getContainerStyleBySize(normalizedSize);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, sizeStyles.container, style]}>
|
||||
<View style={styles.robotContainer}>
|
||||
<View style={styles.antennasContainer}>
|
||||
<View style={[styles.antenna, styles.antennaLeft, sizeStyles.antenna]} />
|
||||
<View style={[styles.antenna, styles.antennaRight, sizeStyles.antenna]} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.head, sizeStyles.head]}>
|
||||
<View style={styles.eyesContainer}>
|
||||
<View style={[styles.eye, sizeStyles.eye]} />
|
||||
<View style={[styles.eye, sizeStyles.eye]} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[styles.container, containerSizeStyle, style]}>
|
||||
<Image source={APP_LOGO} style={styles.image} resizeMode="contain" accessibilityLabel="Логотип приложения" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,43 +7,41 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
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"
|
||||
};
|
||||
const React = require('react'); //React и хуки
|
||||
const { Modal, View, Text, Pressable } = require('react-native'); //Базовые компоненты
|
||||
const { APP_MESSAGE_VARIANT } = require('../../config/messagingConfig'); //Типы сообщений
|
||||
const styles = require('../../styles/common/AppMessage.styles'); //Стили сообщения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Стиль кнопки сообщения при нажатии
|
||||
function getMessageButtonPressableStyle(stylesRef, buttonStyle) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [stylesRef.buttonBase, pressed && stylesRef.buttonPressed, buttonStyle];
|
||||
};
|
||||
}
|
||||
|
||||
//Кнопка сообщения
|
||||
function AppMessageButton({ title, onPress, onDismiss, buttonStyle, textStyle }) {
|
||||
//Обработчик нажатия - вызывает onPress и закрывает диалог
|
||||
const handlePress = React.useCallback(() => {
|
||||
//Сначала закрываем диалог
|
||||
if (typeof onDismiss === "function") {
|
||||
if (typeof onDismiss === 'function') {
|
||||
onDismiss();
|
||||
}
|
||||
|
||||
//Затем выполняем действие кнопки
|
||||
if (typeof onPress === "function") {
|
||||
if (typeof onPress === 'function') {
|
||||
onPress();
|
||||
}
|
||||
}, [onPress, onDismiss]);
|
||||
|
||||
const getPressableStyle = React.useMemo(() => getMessageButtonPressableStyle(styles, buttonStyle), [buttonStyle]);
|
||||
|
||||
return (
|
||||
<Pressable style={({ pressed }) => [styles.buttonBase, pressed && styles.buttonPressed, buttonStyle]} onPress={handlePress}>
|
||||
<Pressable style={getPressableStyle} onPress={handlePress}>
|
||||
<Text style={[styles.buttonText, textStyle]}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@ -81,28 +79,16 @@ function AppMessage({
|
||||
|
||||
//Обработчик закрытия
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (typeof onRequestClose === "function") onRequestClose();
|
||||
if (typeof onRequestClose === 'function') onRequestClose();
|
||||
}, [onRequestClose]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={!!visible}
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<Modal animationType="fade" transparent={true} visible={!!visible} onRequestClose={handleClose}>
|
||||
<View style={styles.backdrop}>
|
||||
<View style={[styles.container, containerVariantStyle, containerStyle]}>
|
||||
<View style={[styles.header, headerStyle]}>
|
||||
<Text style={[styles.title, titleStyle]}>
|
||||
{title || ""}
|
||||
</Text>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Закрыть сообщение"
|
||||
onPress={handleClose}
|
||||
style={styles.closeButton}
|
||||
>
|
||||
<Text style={[styles.title, titleStyle]}>{title || ''}</Text>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть сообщение" onPress={handleClose} style={styles.closeButton}>
|
||||
<Text style={styles.closeButtonText}>×</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
@ -137,4 +123,3 @@ module.exports = {
|
||||
AppMessage,
|
||||
APP_MESSAGE_VARIANT
|
||||
};
|
||||
|
||||
|
||||
64
rn/app/src/components/common/AppSwitch.js
Normal file
64
rn/app/src/components/common/AppSwitch.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Компонент переключателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { Switch, Pressable } = require('react-native');
|
||||
const AppText = require('./AppText');
|
||||
const styles = require('../../styles/common/AppSwitch.styles');
|
||||
const { APP_COLORS } = require('../../config/theme');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Компонент переключателя с меткой
|
||||
function AppSwitch({ label, value, onValueChange, disabled = false, style, labelStyle }) {
|
||||
//Обработчик изменения значения
|
||||
const handleValueChange = React.useCallback(
|
||||
newValue => {
|
||||
if (!disabled && typeof onValueChange === 'function') {
|
||||
onValueChange(newValue);
|
||||
}
|
||||
},
|
||||
[disabled, onValueChange]
|
||||
);
|
||||
|
||||
//Обработчик нажатия на контейнер (переключение значения)
|
||||
const handlePress = React.useCallback(() => {
|
||||
handleValueChange(!value);
|
||||
}, [handleValueChange, value]);
|
||||
|
||||
return (
|
||||
<Pressable style={[styles.container, disabled && styles.containerDisabled, style]} onPress={handlePress} disabled={disabled}>
|
||||
{label ? (
|
||||
<AppText style={[styles.label, disabled && styles.labelDisabled, labelStyle]} numberOfLines={2}>
|
||||
{label}
|
||||
</AppText>
|
||||
) : null}
|
||||
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={disabled}
|
||||
trackColor={{
|
||||
false: APP_COLORS.borderMedium,
|
||||
true: APP_COLORS.primaryLight
|
||||
}}
|
||||
thumbColor={value ? APP_COLORS.primary : APP_COLORS.white}
|
||||
ios_backgroundColor={APP_COLORS.borderMedium}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = AppSwitch;
|
||||
@ -16,7 +16,7 @@ const styles = require('../../styles/common/CopyButton.styles'); //Стили к
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Иконка копирования (два прямоугольника)
|
||||
//Иконка копирования
|
||||
function CopyIcon() {
|
||||
return (
|
||||
<View style={styles.iconContainer}>
|
||||
@ -26,6 +26,13 @@ function CopyIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
//Стиль кнопки копирования при нажатии
|
||||
function getCopyButtonPressableStyle(stylesRef, disabled, style) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [stylesRef.button, pressed && !disabled && stylesRef.buttonPressed, disabled && stylesRef.buttonDisabled, style];
|
||||
};
|
||||
}
|
||||
|
||||
//Кнопка копирования в буфер обмена
|
||||
function CopyButton({ value, onCopy, onError, disabled = false, style }) {
|
||||
//Обработчик нажатия
|
||||
@ -49,11 +56,13 @@ function CopyButton({ value, onCopy, onError, disabled = false, style }) {
|
||||
}
|
||||
}, [value, disabled, onCopy, onError]);
|
||||
|
||||
const getPressableStyle = React.useMemo(() => getCopyButtonPressableStyle(styles, disabled, style), [disabled, style]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Копировать в буфер обмена"
|
||||
style={({ pressed }) => [styles.button, pressed && !disabled && styles.buttonPressed, disabled && styles.buttonDisabled, style]}
|
||||
style={getPressableStyle}
|
||||
onPress={handlePress}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
45
rn/app/src/components/common/LoadingOverlay.js
Normal file
45
rn/app/src/components/common/LoadingOverlay.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Компонент оверлея загрузки (блокировка экрана)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { Modal, View, ActivityIndicator } = require('react-native');
|
||||
const AppText = require('./AppText');
|
||||
const styles = require('../../styles/common/LoadingOverlay.styles');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Компонент оверлея загрузки
|
||||
function LoadingOverlay({ visible, message = 'Загрузка...' }) {
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent={true} animationType="fade" statusBarTranslucent={true}>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator size="large" color={styles.indicator.color} />
|
||||
{message ? (
|
||||
<AppText style={styles.message} variant="body">
|
||||
{message}
|
||||
</AppText>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = LoadingOverlay;
|
||||
136
rn/app/src/components/common/PasswordInput.js
Normal file
136
rn/app/src/components/common/PasswordInput.js
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Компонент ввода пароля с возможностью показать/скрыть
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { View, TextInput, Pressable } = require('react-native');
|
||||
const AppText = require('./AppText');
|
||||
const styles = require('../../styles/common/PasswordInput.styles');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Компонент ввода пароля
|
||||
const PasswordInput = React.forwardRef(function PasswordInput(
|
||||
{
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
showPassword = false,
|
||||
onTogglePassword,
|
||||
error,
|
||||
helperText,
|
||||
disabled = false,
|
||||
style,
|
||||
inputStyle,
|
||||
labelStyle,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
|
||||
const inputRef = React.useRef(null);
|
||||
const setRef = React.useCallback(
|
||||
node => {
|
||||
inputRef.current = node;
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else if (ref) {
|
||||
ref.current = node;
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
//Обработчик фокуса
|
||||
const handleFocus = React.useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
//Обработчик потери фокуса
|
||||
const handleBlur = React.useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
//Обработчик переключения видимости пароля
|
||||
const handleTogglePassword = React.useCallback(() => {
|
||||
if (typeof onTogglePassword === 'function') {
|
||||
onTogglePassword(!showPassword);
|
||||
}
|
||||
}, [onTogglePassword, showPassword]);
|
||||
|
||||
//Обработчик нажатия на контейнер для фокуса на поле ввода
|
||||
const handleContainerPress = React.useCallback(() => {
|
||||
if (!disabled && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{label ? (
|
||||
<AppText style={[styles.label, labelStyle]} variant="caption" weight="medium">
|
||||
{label}
|
||||
</AppText>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
style={[
|
||||
styles.inputContainer,
|
||||
isFocused && styles.inputContainerFocused,
|
||||
error && styles.inputContainerError,
|
||||
disabled && styles.inputContainerDisabled
|
||||
]}
|
||||
onPress={handleContainerPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<TextInput
|
||||
ref={setRef}
|
||||
style={[styles.input, disabled && styles.inputDisabled, inputStyle]}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={styles.placeholder.color}
|
||||
secureTextEntry={!showPassword}
|
||||
keyboardType="default"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus={false}
|
||||
editable={!disabled}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
accessible={true}
|
||||
accessibilityLabel={label || placeholder}
|
||||
accessibilityRole="text"
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
<Pressable style={styles.toggleButton} onPress={handleTogglePassword} disabled={disabled}>
|
||||
<AppText style={[styles.toggleButtonText, disabled && styles.toggleButtonTextDisabled]}>
|
||||
{showPassword ? 'Скрыть' : 'Показать'}
|
||||
</AppText>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
|
||||
{error || helperText ? (
|
||||
<AppText style={[styles.helperText, error && styles.helperTextError]} variant="caption">
|
||||
{error || helperText}
|
||||
</AppText>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = PasswordInput;
|
||||
@ -7,10 +7,10 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require("react"); //React
|
||||
const { View } = require("react-native"); //Базовые компоненты
|
||||
const AppText = require("../common/AppText"); //Общий текстовый компонент
|
||||
const styles = require("../../styles/inspections/InspectionItem.styles"); //Стили элемента
|
||||
const React = require('react'); //React
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const AppText = require('../common/AppText'); //Общий текстовый компонент
|
||||
const styles = require('../../styles/inspections/InspectionItem.styles'); //Стили элемента
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -34,4 +34,3 @@ function InspectionItem({ item }) {
|
||||
//----------------
|
||||
|
||||
module.exports = InspectionItem;
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ function InspectionList({ inspections, isLoading, error, onRefresh }) {
|
||||
if (!hasData && isLoading) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="small" color="#2563EB" />
|
||||
<ActivityIndicator size="small" color={styles.indicator.color} />
|
||||
<AppText style={styles.centerText}>Загружаем данные...</AppText>
|
||||
</View>
|
||||
);
|
||||
|
||||
124
rn/app/src/components/layout/AppAuthProvider.js
Normal file
124
rn/app/src/components/layout/AppAuthProvider.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Провайдер авторизации приложения
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const useAuth = require('../../hooks/useAuth');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Контекст авторизации
|
||||
const AppAuthContext = React.createContext(null);
|
||||
|
||||
//Провайдер авторизации
|
||||
function AppAuthProvider({ children }) {
|
||||
const auth = useAuth();
|
||||
|
||||
//Состояние для хранения данных формы авторизации
|
||||
const [authFormData, setAuthFormData] = React.useState({
|
||||
serverUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
savePassword: false
|
||||
});
|
||||
|
||||
//Флаг проверки сессии
|
||||
const [sessionChecked, setSessionChecked] = React.useState(false);
|
||||
|
||||
//Обновление данных формы авторизации
|
||||
const updateAuthFormData = React.useCallback(data => {
|
||||
setAuthFormData(prev => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
//Очистка данных формы авторизации
|
||||
const clearAuthFormData = React.useCallback(() => {
|
||||
setAuthFormData({
|
||||
serverUrl: '',
|
||||
username: '',
|
||||
password: '',
|
||||
savePassword: false
|
||||
});
|
||||
}, []);
|
||||
|
||||
//Отметка что сессия была проверена
|
||||
const markSessionChecked = React.useCallback(() => {
|
||||
setSessionChecked(true);
|
||||
}, []);
|
||||
|
||||
//Мемоизация значения контекста с перечислением отдельных свойств
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
session: auth.session,
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
isLoading: auth.isLoading,
|
||||
isInitialized: auth.isInitialized,
|
||||
error: auth.error,
|
||||
login: auth.login,
|
||||
logout: auth.logout,
|
||||
selectCompany: auth.selectCompany,
|
||||
checkSession: auth.checkSession,
|
||||
getDeviceId: auth.getDeviceId,
|
||||
getSavedCredentials: auth.getSavedCredentials,
|
||||
clearSavedCredentials: auth.clearSavedCredentials,
|
||||
getAuthSession: auth.getAuthSession,
|
||||
clearAuthSession: auth.clearAuthSession,
|
||||
getAndClearSessionRestoredFromStorage: auth.getAndClearSessionRestoredFromStorage,
|
||||
AUTH_SETTINGS_KEYS: auth.AUTH_SETTINGS_KEYS,
|
||||
authFormData,
|
||||
updateAuthFormData,
|
||||
clearAuthFormData,
|
||||
sessionChecked,
|
||||
markSessionChecked
|
||||
}),
|
||||
[
|
||||
auth.session,
|
||||
auth.isAuthenticated,
|
||||
auth.isLoading,
|
||||
auth.isInitialized,
|
||||
auth.error,
|
||||
auth.login,
|
||||
auth.logout,
|
||||
auth.selectCompany,
|
||||
auth.checkSession,
|
||||
auth.getDeviceId,
|
||||
auth.getSavedCredentials,
|
||||
auth.clearSavedCredentials,
|
||||
auth.getAuthSession,
|
||||
auth.clearAuthSession,
|
||||
auth.getAndClearSessionRestoredFromStorage,
|
||||
auth.AUTH_SETTINGS_KEYS,
|
||||
authFormData,
|
||||
updateAuthFormData,
|
||||
clearAuthFormData,
|
||||
sessionChecked,
|
||||
markSessionChecked
|
||||
]
|
||||
);
|
||||
|
||||
return <AppAuthContext.Provider value={value}>{children}</AppAuthContext.Provider>;
|
||||
}
|
||||
|
||||
//Хук доступа к контексту авторизации
|
||||
function useAppAuthContext() {
|
||||
const ctx = React.useContext(AppAuthContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAppAuthContext должен использоваться внутри AppAuthProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
AppAuthProvider,
|
||||
useAppAuthContext
|
||||
};
|
||||
@ -17,20 +17,24 @@ const styles = require('../../styles/layout/AppErrorBoundary.styles'); //Сти
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обработчик перезагрузки приложения
|
||||
function handleErrorReload(onReload) {
|
||||
if (typeof onReload === 'function') {
|
||||
onReload();
|
||||
return;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.location && typeof window.location.reload === 'function') {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
//Компонент страницы ошибки
|
||||
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();
|
||||
}
|
||||
};
|
||||
const handleReload = React.useCallback(() => {
|
||||
handleErrorReload(onReload);
|
||||
}, [onReload]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@ -13,52 +13,12 @@ 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 (
|
||||
<Pressable style={({ pressed }) => [styles.modeContainer, styleConfig.color, pressed && styles.modePressed]} onPress={onPress}>
|
||||
<AppText style={[styles.modeText, styleConfig.textColor]}>{label}</AppText>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//Иконка стрелки назад
|
||||
function BackArrowIcon() {
|
||||
return (
|
||||
@ -69,33 +29,28 @@ function BackArrowIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
//Стиль кнопки назад при нажатии
|
||||
function getBackButtonPressableStyle(stylesRef) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [stylesRef.backButton, pressed && stylesRef.backButtonPressed];
|
||||
};
|
||||
}
|
||||
|
||||
//Кнопка назад
|
||||
function BackButton({ onPress }) {
|
||||
const getPressableStyle = React.useMemo(() => getBackButtonPressableStyle(styles), []);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Назад"
|
||||
style={({ pressed }) => [styles.backButton, pressed && styles.backButtonPressed]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Назад" style={getPressableStyle} onPress={onPress}>
|
||||
<BackArrowIcon />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
//Заголовок приложения
|
||||
function AppHeader({
|
||||
title,
|
||||
subtitle,
|
||||
showMenuButton = true,
|
||||
onMenuPress,
|
||||
showModeIndicator = true,
|
||||
showBackButton = false,
|
||||
onBackPress
|
||||
}) {
|
||||
function AppHeader({ title, subtitle, showMenuButton = true, onMenuPress, showBackButton = false, onBackPress }) {
|
||||
const { mode } = useAppModeContext();
|
||||
const { currentScreen, SCREENS } = useAppNavigationContext();
|
||||
const { showInfo } = useAppMessagingContext();
|
||||
|
||||
//Получение заголовка экрана
|
||||
const getTitle = React.useCallback(() => {
|
||||
@ -125,15 +80,12 @@ function AppHeader({
|
||||
}
|
||||
}, [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 getMenuButtonPressableStyle = React.useMemo(() => {
|
||||
return function styleFn({ pressed }) {
|
||||
return [styles.menuButton, pressed && styles.menuButtonPressed];
|
||||
};
|
||||
}, []);
|
||||
|
||||
//Отрисовка левой части шапки (логотип или кнопка назад)
|
||||
const renderLeftSection = () => {
|
||||
@ -160,19 +112,15 @@ function AppHeader({
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.controls}>
|
||||
{showModeIndicator ? <ModeIndicator mode={mode} onPress={handleModeIndicatorPress} /> : null}
|
||||
|
||||
{showMenuButton ? (
|
||||
<Pressable style={({ pressed }) => [styles.menuButton, pressed && styles.menuButtonPressed]} onPress={onMenuPress}>
|
||||
<View style={styles.menuButtonIcon}>
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
</View>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
{showMenuButton ? (
|
||||
<Pressable style={getMenuButtonPressableStyle} onPress={onMenuPress}>
|
||||
<View style={styles.menuButtonIcon}>
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
<View style={styles.menuButtonIconLine} />
|
||||
</View>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -36,7 +36,10 @@ function AppLocalDbProvider({ children }) {
|
||||
clearSettings: api.clearSettings,
|
||||
clearInspections: api.clearInspections,
|
||||
vacuum: api.vacuum,
|
||||
checkTableExists: api.checkTableExists
|
||||
checkTableExists: api.checkTableExists,
|
||||
setAuthSession: api.setAuthSession,
|
||||
getAuthSession: api.getAuthSession,
|
||||
clearAuthSession: api.clearAuthSession
|
||||
}),
|
||||
[
|
||||
api.isDbReady,
|
||||
@ -51,7 +54,10 @@ function AppLocalDbProvider({ children }) {
|
||||
api.clearSettings,
|
||||
api.clearInspections,
|
||||
api.vacuum,
|
||||
api.checkTableExists
|
||||
api.checkTableExists,
|
||||
api.setAuthSession,
|
||||
api.getAuthSession,
|
||||
api.clearAuthSession
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ function AppNavigationProvider({ children }) {
|
||||
}, [canGoBack, goBack]);
|
||||
|
||||
//Подключаем обработчик кнопки "Назад"
|
||||
useHardwareBackPress(handleHardwareBackPress, [handleHardwareBackPress]);
|
||||
useHardwareBackPress(handleHardwareBackPress);
|
||||
|
||||
//Мемоизация значения контекста с перечислением отдельных свойств
|
||||
const value = React.useMemo(
|
||||
@ -49,6 +49,7 @@ function AppNavigationProvider({ children }) {
|
||||
navigate: navigationApi.navigate,
|
||||
goBack: navigationApi.goBack,
|
||||
reset: navigationApi.reset,
|
||||
setInitialScreen: navigationApi.setInitialScreen,
|
||||
canGoBack: navigationApi.canGoBack
|
||||
}),
|
||||
[
|
||||
@ -58,6 +59,7 @@ function AppNavigationProvider({ children }) {
|
||||
navigationApi.navigate,
|
||||
navigationApi.goBack,
|
||||
navigationApi.reset,
|
||||
navigationApi.setInitialScreen,
|
||||
navigationApi.canGoBack
|
||||
]
|
||||
);
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { usePreTripInspections } = require('../../hooks/usePreTripInspections'); //Хук предметной области
|
||||
const usePreTripInspections = require('../../hooks/usePreTripInspections'); //Хук предметной области
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
|
||||
@ -11,6 +11,9 @@ const React = require('react'); //React и хуки
|
||||
const { useColorScheme } = require('react-native'); //Определение темы устройства
|
||||
const { SafeAreaProvider } = require('react-native-safe-area-context'); //Провайдер безопасной области
|
||||
const AppShell = require('./AppShell'); //Оболочка приложения
|
||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||
const { useAppAuthContext } = require('./AppAuthProvider'); //Контекст авторизации
|
||||
const { useAppLocalDbContext } = require('./AppLocalDbProvider'); //Контекст локальной БД
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -21,6 +24,28 @@ function AppRoot() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDarkMode = colorScheme === 'dark';
|
||||
|
||||
const { setInitialScreen, SCREENS } = useAppNavigationContext();
|
||||
const { isAuthenticated, isInitialized } = useAppAuthContext();
|
||||
const { isDbReady } = useAppLocalDbContext();
|
||||
|
||||
//Флаг для предотвращения повторной установки начального экрана
|
||||
const initialScreenSetRef = React.useRef(false);
|
||||
|
||||
//Установка начального экрана при готовности
|
||||
React.useEffect(() => {
|
||||
//Ждём инициализации БД и авторизации
|
||||
if (!isDbReady || !isInitialized || initialScreenSetRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
initialScreenSetRef.current = true;
|
||||
|
||||
//Если авторизован - показываем главный экран
|
||||
if (isAuthenticated) {
|
||||
setInitialScreen(SCREENS.MAIN);
|
||||
}
|
||||
}, [isDbReady, isInitialized, isAuthenticated, setInitialScreen, SCREENS.MAIN]);
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AppShell isDarkMode={isDarkMode} />
|
||||
|
||||
@ -12,6 +12,7 @@ const { StatusBar, Platform } = require('react-native'); //Базовые ком
|
||||
const { useAppNavigationContext } = require('./AppNavigationProvider'); //Контекст навигации
|
||||
const MainScreen = require('../../screens/MainScreen'); //Главный экран
|
||||
const SettingsScreen = require('../../screens/SettingsScreen'); //Экран настроек
|
||||
const AuthScreen = require('../../screens/AuthScreen'); //Экран авторизации
|
||||
const AdaptiveView = require('../common/AdaptiveView'); //Адаптивный контейнер
|
||||
const styles = require('../../styles/layout/AppShell.styles'); //Стили оболочки
|
||||
|
||||
@ -29,10 +30,12 @@ function AppShell({ isDarkMode }) {
|
||||
return <MainScreen />;
|
||||
case SCREENS.SETTINGS:
|
||||
return <SettingsScreen />;
|
||||
case SCREENS.AUTH:
|
||||
return <AuthScreen />;
|
||||
default:
|
||||
return <MainScreen />;
|
||||
}
|
||||
}, [currentScreen, SCREENS.MAIN, SCREENS.SETTINGS]);
|
||||
}, [currentScreen, SCREENS.MAIN, SCREENS.SETTINGS, SCREENS.AUTH]);
|
||||
|
||||
//Определяем цвет status bar в зависимости от темы
|
||||
const statusBarStyle = isDarkMode ? 'light-content' : 'dark-content';
|
||||
|
||||
@ -16,6 +16,13 @@ const styles = require('../../styles/menu/MenuHeader.styles'); //Стили за
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Стиль кнопки закрытия меню при нажатии
|
||||
function getMenuCloseButtonPressableStyle(stylesRef) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [stylesRef.closeButton, pressed && stylesRef.closeButtonPressed];
|
||||
};
|
||||
}
|
||||
|
||||
//Заголовок меню
|
||||
function MenuHeader({ title, onClose, showCloseButton = true, style }) {
|
||||
const handleClose = React.useCallback(() => {
|
||||
@ -24,18 +31,15 @@ function MenuHeader({ title, onClose, showCloseButton = true, style }) {
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
const getCloseButtonPressableStyle = React.useMemo(() => getMenuCloseButtonPressableStyle(styles), []);
|
||||
|
||||
return (
|
||||
<View style={[styles.header, style]}>
|
||||
<AppText style={styles.title} numberOfLines={2}>
|
||||
{title || 'Меню'}
|
||||
</AppText>
|
||||
{showCloseButton ? (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Закрыть меню"
|
||||
onPress={handleClose}
|
||||
style={({ pressed }) => [styles.closeButton, pressed && styles.closeButtonPressed]}
|
||||
>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Закрыть меню" onPress={handleClose} style={getCloseButtonPressableStyle}>
|
||||
<View style={styles.closeButtonIcon}>
|
||||
<AppText style={styles.closeButtonText}>×</AppText>
|
||||
</View>
|
||||
|
||||
@ -16,6 +16,19 @@ const styles = require('../../styles/menu/MenuItem.styles'); //Стили эле
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Стиль элемента меню при нажатии
|
||||
function getMenuItemPressableStyle(stylesRef, isDestructive, disabled, style) {
|
||||
return function styleFn({ pressed }) {
|
||||
return [
|
||||
stylesRef.menuItem,
|
||||
pressed && !disabled && stylesRef.menuItemPressed,
|
||||
isDestructive && stylesRef.menuItemDestructive,
|
||||
disabled && stylesRef.menuItemDisabled,
|
||||
style
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
//Элемент меню
|
||||
function MenuItem({ title, icon, onPress, isDestructive = false, disabled = false, style, textStyle }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
@ -24,18 +37,13 @@ function MenuItem({ title, icon, onPress, isDestructive = false, disabled = fals
|
||||
}
|
||||
}, [disabled, onPress]);
|
||||
|
||||
const getPressableStyle = React.useMemo(
|
||||
() => getMenuItemPressableStyle(styles, isDestructive, disabled, style),
|
||||
[isDestructive, disabled, style]
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.menuItem,
|
||||
pressed && !disabled && styles.menuItemPressed,
|
||||
isDestructive && styles.menuItemDestructive,
|
||||
disabled && styles.menuItemDisabled,
|
||||
style
|
||||
]}
|
||||
onPress={handlePress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Pressable style={getPressableStyle} onPress={handlePress} disabled={disabled}>
|
||||
<View style={styles.menuItemContent}>
|
||||
{icon ? <View style={styles.menuItemIcon}>{icon}</View> : null}
|
||||
<AppText
|
||||
|
||||
@ -18,6 +18,28 @@ const styles = require('../../styles/menu/MenuList.styles'); //Стили спи
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Строка меню с элементом
|
||||
function MenuItemRow({ item, index, items, onItemPress }) {
|
||||
const handlePress = React.useCallback(() => {
|
||||
onItemPress(item);
|
||||
}, [item, onItemPress]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<MenuItem
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
onPress={handlePress}
|
||||
isDestructive={item.isDestructive}
|
||||
disabled={item.disabled}
|
||||
style={item.style}
|
||||
textStyle={item.textStyle}
|
||||
/>
|
||||
{item.showDivider && index < items.length - 1 && <MenuDivider />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//Список элементов меню
|
||||
function MenuList({ items = [], onClose, style }) {
|
||||
const handleItemPress = React.useCallback(
|
||||
@ -38,20 +60,15 @@ function MenuList({ items = [], onClose, style }) {
|
||||
|
||||
return (
|
||||
<ScrollView style={[styles.scrollView, style]} showsVerticalScrollIndicator={false} bounces={false}>
|
||||
{items.map((item, index) => (
|
||||
<View key={item.id || `menu-item-${index}`}>
|
||||
<MenuItem
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
onPress={() => handleItemPress(item)}
|
||||
isDestructive={item.isDestructive}
|
||||
disabled={item.disabled}
|
||||
style={item.style}
|
||||
textStyle={item.textStyle}
|
||||
/>
|
||||
{item.showDivider && index < items.length - 1 && <MenuDivider />}
|
||||
</View>
|
||||
))}
|
||||
{items.map((item, index) => {
|
||||
//Элемент-разделитель
|
||||
if (item.type === 'divider') {
|
||||
return <MenuDivider key={item.id || `menu-divider-${index}`} />;
|
||||
}
|
||||
|
||||
//Обычный элемент меню
|
||||
return <MenuItemRow key={item.id || `menu-item-${index}`} item={item} index={index} items={items} onItemPress={handleItemPress} />;
|
||||
})}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
83
rn/app/src/components/menu/MenuUserInfo.js
Normal file
83
rn/app/src/components/menu/MenuUserInfo.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Компонент информации о пользователе и режиме работы в меню
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { View } = require('react-native');
|
||||
const AppText = require('../common/AppText');
|
||||
const { getModeLabel } = require('../../utils/appInfo');
|
||||
const styles = require('../../styles/menu/MenuUserInfo.styles');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Компонент информации о пользователе и режиме работы
|
||||
function MenuUserInfo({ mode, username, organization, isConnected }) {
|
||||
//Получение стиля для индикатора режима
|
||||
const getModeStyle = React.useCallback(() => {
|
||||
switch (mode) {
|
||||
case 'ONLINE':
|
||||
return styles.modeOnline;
|
||||
case 'OFFLINE':
|
||||
return styles.modeOffline;
|
||||
case 'NOT_CONNECTED':
|
||||
default:
|
||||
return styles.modeNotConnected;
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
//Получение стиля текста для режима
|
||||
const getModeTextStyle = React.useCallback(() => {
|
||||
switch (mode) {
|
||||
case 'ONLINE':
|
||||
return styles.modeTextOnline;
|
||||
case 'OFFLINE':
|
||||
return styles.modeTextOffline;
|
||||
case 'NOT_CONNECTED':
|
||||
default:
|
||||
return styles.modeTextNotConnected;
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.modeContainer, getModeStyle()]}>
|
||||
<AppText style={[styles.modeText, getModeTextStyle()]}>{getModeLabel(mode)}</AppText>
|
||||
</View>
|
||||
|
||||
{isConnected && (username || organization) ? (
|
||||
<View style={styles.userInfo}>
|
||||
{username ? (
|
||||
<View style={styles.userRow}>
|
||||
<AppText style={styles.userLabel}>Пользователь:</AppText>
|
||||
<AppText style={styles.userValue} numberOfLines={1}>
|
||||
{username}
|
||||
</AppText>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{organization ? (
|
||||
<View style={styles.userRow}>
|
||||
<AppText style={styles.userLabel}>Организация:</AppText>
|
||||
<AppText style={styles.userValue} numberOfLines={1}>
|
||||
{organization}
|
||||
</AppText>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = MenuUserInfo;
|
||||
@ -11,24 +11,23 @@ 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 MenuUserInfo = require('./MenuUserInfo'); //Информация о пользователе
|
||||
const MenuList = require('./MenuList'); //Список элементов меню
|
||||
const { widthPercentage, isTablet } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
const {
|
||||
MENU_WIDTH_PHONE_PERCENT,
|
||||
MENU_WIDTH_TABLET_PERCENT,
|
||||
MENU_MAX_WIDTH,
|
||||
MENU_MIN_WIDTH,
|
||||
ANIMATION_DURATION_OPEN,
|
||||
ANIMATION_DURATION_CLOSE
|
||||
} = require('../../config/menuConfig'); //Конфигурация меню
|
||||
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;
|
||||
@ -37,7 +36,9 @@ const calculateMenuWidth = () => {
|
||||
};
|
||||
|
||||
//Боковое меню приложения
|
||||
function SideMenu({ visible, onClose, items = [], title = 'Меню', headerStyle, containerStyle, contentStyle }) {
|
||||
function SideMenu({ visible, onClose, items = [], title = 'Меню', mode, username, organization, headerStyle, containerStyle, contentStyle }) {
|
||||
//Определение подключено ли приложение
|
||||
const isConnected = mode === 'ONLINE' || mode === 'OFFLINE';
|
||||
//Получаем отступы безопасной области
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@ -123,13 +124,7 @@ function SideMenu({ visible, onClose, items = [], title = 'Меню', headerStyl
|
||||
}, [visible, modalVisible, openMenu, closeMenu]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
transparent={true}
|
||||
animationType="none"
|
||||
statusBarTranslucent={true}
|
||||
onRequestClose={handleRequestClose}
|
||||
>
|
||||
<Modal visible={modalVisible} transparent={true} animationType="none" statusBarTranslucent={true} onRequestClose={handleRequestClose}>
|
||||
<View style={styles.modalContainer}>
|
||||
<Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]}>
|
||||
<Pressable style={styles.backdropPressable} onPress={handleBackdropPress} />
|
||||
@ -148,6 +143,8 @@ function SideMenu({ visible, onClose, items = [], title = 'Меню', headerStyl
|
||||
>
|
||||
<MenuHeader title={title} onClose={onClose} style={[styles.header, headerStyle]} />
|
||||
|
||||
<MenuUserInfo mode={mode} username={username} organization={organization} isConnected={isConnected} />
|
||||
|
||||
<MenuList items={items} onClose={onClose} style={contentStyle} />
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
27
rn/app/src/config/appAssets.js
Normal file
27
rn/app/src/config/appAssets.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация ресурсов приложения (изображения, иконки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Ключи размеров логотипа
|
||||
const LOGO_SIZE_KEYS = Object.freeze({
|
||||
SMALL: 'small',
|
||||
MEDIUM: 'medium',
|
||||
LARGE: 'large'
|
||||
});
|
||||
|
||||
//Изображение логотипа приложения
|
||||
const APP_LOGO = require('../../assets/icons/logo.png');
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
LOGO_SIZE_KEYS,
|
||||
APP_LOGO
|
||||
};
|
||||
@ -14,30 +14,6 @@ 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 = {
|
||||
//Отступы по умолчанию (адаптивные)
|
||||
@ -89,8 +65,6 @@ const COMPATIBILITY = {
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
SYSTEM,
|
||||
LOCAL_DB,
|
||||
UI,
|
||||
COMPATIBILITY
|
||||
};
|
||||
|
||||
42
rn/app/src/config/authApi.js
Normal file
42
rn/app/src/config/authApi.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Константы API авторизации и сообщения об ошибках
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Коды действий API авторизации
|
||||
const ACTION_CODES = {
|
||||
LOG_IN: 'LOG_IN',
|
||||
LOG_OUT: 'LOG_OUT',
|
||||
CHECK_SESSION: 'CHECK_SESSION',
|
||||
SET_COMPANY: 'SET_COMPANY'
|
||||
};
|
||||
|
||||
//Статусы ответа сервера
|
||||
const RESPONSE_STATES = {
|
||||
OK: 'OK',
|
||||
ERR: 'ERR'
|
||||
};
|
||||
|
||||
//Сообщения об ошибках авторизации
|
||||
const ERROR_MESSAGES = {
|
||||
NETWORK_ERROR: 'Ошибка соединения с сервером',
|
||||
SERVER_ERROR: 'Ошибка сервера',
|
||||
INVALID_RESPONSE: 'Некорректный ответ сервера',
|
||||
SESSION_EXPIRED: 'Сессия истекла',
|
||||
LOGIN_FAILED: 'Ошибка входа',
|
||||
LOGOUT_FAILED: 'Ошибка выхода'
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
ACTION_CODES,
|
||||
RESPONSE_STATES,
|
||||
ERROR_MESSAGES
|
||||
};
|
||||
44
rn/app/src/config/authConfig.js
Normal file
44
rn/app/src/config/authConfig.js
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация авторизации
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Ключи настроек авторизации
|
||||
const AUTH_SETTINGS_KEYS = {
|
||||
SERVER_URL: 'app_server_url',
|
||||
HIDE_SERVER_URL: 'auth_hide_server_url',
|
||||
IDLE_TIMEOUT: 'auth_idle_timeout',
|
||||
DEVICE_ID: 'auth_device_id',
|
||||
DEVICE_SECRET_KEY: 'auth_device_secret_key',
|
||||
SAVED_LOGIN: 'auth_saved_login',
|
||||
SAVED_PASSWORD: 'auth_saved_password',
|
||||
SAVE_PASSWORD_ENABLED: 'auth_save_password_enabled'
|
||||
};
|
||||
|
||||
//Значение времени простоя по умолчанию (минуты)
|
||||
const DEFAULT_IDLE_TIMEOUT = 30;
|
||||
|
||||
//Ключи настроек подключения
|
||||
const CONNECTION_SETTINGS_KEYS = [
|
||||
AUTH_SETTINGS_KEYS.SERVER_URL,
|
||||
AUTH_SETTINGS_KEYS.HIDE_SERVER_URL,
|
||||
AUTH_SETTINGS_KEYS.SAVED_LOGIN,
|
||||
AUTH_SETTINGS_KEYS.SAVED_PASSWORD,
|
||||
AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED,
|
||||
AUTH_SETTINGS_KEYS.DEVICE_ID,
|
||||
AUTH_SETTINGS_KEYS.DEVICE_SECRET_KEY
|
||||
];
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
AUTH_SETTINGS_KEYS,
|
||||
CONNECTION_SETTINGS_KEYS,
|
||||
DEFAULT_IDLE_TIMEOUT
|
||||
};
|
||||
19
rn/app/src/config/database.js
Normal file
19
rn/app/src/config/database.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация локальной базы данных
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Имя файла базы данных SQLite
|
||||
const DB_NAME = 'pretrip_inspections.db';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
DB_NAME
|
||||
};
|
||||
56
rn/app/src/config/dialogButtons.js
Normal file
56
rn/app/src/config/dialogButtons.js
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Константы стилей кнопок диалогов подтверждения
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { APP_COLORS } = require('./theme');
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы кнопок подтверждения в диалогах
|
||||
const DIALOG_BUTTON_TYPE = {
|
||||
ERROR: 'error',
|
||||
WARNING: 'warning'
|
||||
};
|
||||
|
||||
//Стили кнопки "Отмена" (нейтральная) — общие для всех диалогов
|
||||
const DIALOG_CANCEL_BUTTON = {
|
||||
id: 'cancel',
|
||||
title: 'Отмена',
|
||||
onPress: () => {}
|
||||
};
|
||||
|
||||
//Фабрика: стили и текст для кнопки подтверждения в диалоге
|
||||
function getConfirmButtonOptions(type, title, onPress) {
|
||||
const base = {
|
||||
id: 'confirm',
|
||||
title: title || 'Подтвердить',
|
||||
onPress: typeof onPress === 'function' ? onPress : () => {}
|
||||
};
|
||||
|
||||
if (type === DIALOG_BUTTON_TYPE.ERROR) {
|
||||
base.buttonStyle = { backgroundColor: APP_COLORS.error };
|
||||
base.textStyle = { color: APP_COLORS.white };
|
||||
} else if (type === DIALOG_BUTTON_TYPE.WARNING) {
|
||||
base.buttonStyle = { backgroundColor: APP_COLORS.warning };
|
||||
base.textStyle = { color: APP_COLORS.white };
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
DIALOG_BUTTON_TYPE,
|
||||
DIALOG_CANCEL_BUTTON,
|
||||
getConfirmButtonOptions
|
||||
};
|
||||
25
rn/app/src/config/loadStatus.js
Normal file
25
rn/app/src/config/loadStatus.js
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Константы статусов загрузки данных
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Статусы загрузки данных
|
||||
const LOAD_STATUS_IDLE = 'IDLE';
|
||||
const LOAD_STATUS_LOADING = 'LOADING';
|
||||
const LOAD_STATUS_DONE = 'DONE';
|
||||
const LOAD_STATUS_ERROR = 'ERROR';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
LOAD_STATUS_IDLE,
|
||||
LOAD_STATUS_LOADING,
|
||||
LOAD_STATUS_DONE,
|
||||
LOAD_STATUS_ERROR
|
||||
};
|
||||
33
rn/app/src/config/menuConfig.js
Normal file
33
rn/app/src/config/menuConfig.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Конфигурация бокового меню
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Ширина меню в процентах от экрана
|
||||
const MENU_WIDTH_PHONE_PERCENT = 70;
|
||||
const MENU_WIDTH_TABLET_PERCENT = 40;
|
||||
|
||||
//Границы ширины меню (px)
|
||||
const MENU_MAX_WIDTH = 360;
|
||||
const MENU_MIN_WIDTH = 280;
|
||||
|
||||
//Длительность анимации (мс)
|
||||
const ANIMATION_DURATION_OPEN = 250;
|
||||
const ANIMATION_DURATION_CLOSE = 200;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
MENU_WIDTH_PHONE_PERCENT,
|
||||
MENU_WIDTH_TABLET_PERCENT,
|
||||
MENU_MAX_WIDTH,
|
||||
MENU_MIN_WIDTH,
|
||||
ANIMATION_DURATION_OPEN,
|
||||
ANIMATION_DURATION_CLOSE
|
||||
};
|
||||
23
rn/app/src/config/messages.js
Normal file
23
rn/app/src/config/messages.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Тексты сообщений приложения
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Сообщение при потере связи с сервером и переходе в офлайн
|
||||
const CONNECTION_LOST_MESSAGE = 'Нет связи с сервером. Приложение переведено в режим офлайн.';
|
||||
|
||||
//Заголовок сообщения при переходе в режим офлайн
|
||||
const OFFLINE_MODE_TITLE = 'Режим офлайн';
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
CONNECTION_LOST_MESSAGE,
|
||||
OFFLINE_MODE_TITLE
|
||||
};
|
||||
31
rn/app/src/config/messagingConfig.js
Normal file
31
rn/app/src/config/messagingConfig.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Константы типов сообщений и действий
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Варианты отображения сообщения
|
||||
const APP_MESSAGE_VARIANT = {
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERR: 'ERR',
|
||||
SUCCESS: 'SUCCESS'
|
||||
};
|
||||
|
||||
//Типы действий редьюсера сообщений
|
||||
const MSG_AT = {
|
||||
SHOW_MSG: 'SHOW_MSG',
|
||||
HIDE_MSG: 'HIDE_MSG'
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
APP_MESSAGE_VARIANT,
|
||||
MSG_AT
|
||||
};
|
||||
23
rn/app/src/config/responsiveConfig.js
Normal file
23
rn/app/src/config/responsiveConfig.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Базовые размеры для адаптивных утилит
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Базовая ширина экрана (iPhone 11/12/13/14)
|
||||
const BASE_WIDTH = 375;
|
||||
|
||||
//Базовая высота экрана
|
||||
const BASE_HEIGHT = 812;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
BASE_WIDTH,
|
||||
BASE_HEIGHT
|
||||
};
|
||||
23
rn/app/src/config/routes.js
Normal file
23
rn/app/src/config/routes.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Константы экранов навигации
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Экраны приложения
|
||||
const SCREENS = {
|
||||
MAIN: 'MAIN',
|
||||
SETTINGS: 'SETTINGS',
|
||||
AUTH: 'AUTH'
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
SCREENS
|
||||
};
|
||||
27
rn/app/src/config/storageKeys.js
Normal file
27
rn/app/src/config/storageKeys.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Ключи и константы хранилища и шифрования
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Ключ для хранения идентификатора устройства в localStorage (Web)
|
||||
const WEB_DEVICE_ID_KEY = 'pti_device_id';
|
||||
|
||||
//Префикс для идентификации зашифрованных данных
|
||||
const ENCRYPTED_PREFIX = 'ENC:';
|
||||
|
||||
//Длина генерируемого секретного ключа
|
||||
const SECRET_KEY_LENGTH = 64;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
WEB_DEVICE_ID_KEY,
|
||||
ENCRYPTED_PREFIX,
|
||||
SECRET_KEY_LENGTH
|
||||
};
|
||||
@ -44,6 +44,7 @@ const APP_COLORS = {
|
||||
borderSubtle: '#E2E8F0',
|
||||
borderMedium: '#CBD5E1',
|
||||
overlay: 'rgba(15, 23, 42, 0.5)',
|
||||
shadow: '#000000',
|
||||
|
||||
//Семантические
|
||||
info: '#3B82F6',
|
||||
|
||||
@ -11,12 +11,7 @@ const { open } = require('react-native-quick-sqlite');
|
||||
|
||||
//Импорт утилиты для загрузки SQL файлов
|
||||
const SQLFileLoader = require('./sql/SQLFileLoader');
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
const DB_NAME = 'pretrip_inspections.db';
|
||||
const { DB_NAME } = require('../config/database'); //Имя базы данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -72,6 +67,9 @@ class SQLiteDatabase {
|
||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_INSPECTIONS);
|
||||
console.log('Таблица inspections создана/проверена');
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_TABLE_AUTH_SESSION);
|
||||
console.log('Таблица auth_session создана/проверена');
|
||||
|
||||
await this.executeQuery(this.sqlQueries.CREATE_INDEX_INSPECTIONS_STATUS);
|
||||
console.log('Индекс idx_inspections_status создан/проверен');
|
||||
|
||||
@ -303,6 +301,75 @@ class SQLiteDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
//Сохранение сессии авторизации
|
||||
async setAuthSession(session) {
|
||||
try {
|
||||
const {
|
||||
sessionId,
|
||||
serverUrl,
|
||||
userRn = null,
|
||||
userCode = null,
|
||||
userName = null,
|
||||
companyRn = null,
|
||||
companyName = null,
|
||||
savePassword = false
|
||||
} = session;
|
||||
|
||||
await this.executeQuery(this.sqlQueries.AUTH_SESSION_SET, [
|
||||
sessionId,
|
||||
serverUrl,
|
||||
userRn,
|
||||
userCode,
|
||||
userName,
|
||||
companyRn,
|
||||
companyName,
|
||||
savePassword ? 1 : 0
|
||||
]);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Получение сессии авторизации
|
||||
async getAuthSession() {
|
||||
try {
|
||||
const result = await this.executeQuery(this.sqlQueries.AUTH_SESSION_GET, []);
|
||||
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
const row = result.rows.item(0);
|
||||
return {
|
||||
sessionId: row.session_id,
|
||||
serverUrl: row.server_url,
|
||||
userRn: row.user_rn,
|
||||
userCode: row.user_code,
|
||||
userName: row.user_name,
|
||||
companyRn: row.company_rn,
|
||||
companyName: row.company_name,
|
||||
savePassword: row.save_password === 1,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Очистка сессии авторизации
|
||||
async clearAuthSession() {
|
||||
try {
|
||||
await this.executeQuery(this.sqlQueries.AUTH_SESSION_CLEAR, []);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки сессии авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка существования таблицы
|
||||
async checkTableExists(tableName) {
|
||||
try {
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
//Таблицы
|
||||
const CREATE_TABLE_APP_SETTINGS = require('./settings/create_table_app_settings.sql');
|
||||
const CREATE_TABLE_INSPECTIONS = require('./inspections/create_table_inspections.sql');
|
||||
const CREATE_TABLE_AUTH_SESSION = require('./auth/create_table_auth_session.sql');
|
||||
|
||||
//Индексы
|
||||
const CREATE_INDEX_INSPECTIONS_STATUS = require('./inspections/create_index_inspections_status.sql');
|
||||
@ -31,6 +32,11 @@ const INSPECTIONS_DELETE = require('./inspections/delete_inspection.sql');
|
||||
const INSPECTIONS_DELETE_ALL = require('./inspections/delete_all_inspections.sql');
|
||||
const INSPECTIONS_COUNT = require('./inspections/count_inspections.sql');
|
||||
|
||||
//Авторизация
|
||||
const AUTH_SESSION_SET = require('./auth/set_auth_session.sql');
|
||||
const AUTH_SESSION_GET = require('./auth/get_auth_session.sql');
|
||||
const AUTH_SESSION_CLEAR = require('./auth/clear_auth_session.sql');
|
||||
|
||||
//Утилиты
|
||||
const UTILITY_CHECK_TABLE = require('./utility/check_table_exists.sql');
|
||||
const UTILITY_DROP_TABLE = require('./utility/drop_table.sql');
|
||||
@ -44,6 +50,7 @@ const UTILITY_VACUUM = require('./utility/vacuum.sql');
|
||||
const SQLQueries = {
|
||||
CREATE_TABLE_APP_SETTINGS,
|
||||
CREATE_TABLE_INSPECTIONS,
|
||||
CREATE_TABLE_AUTH_SESSION,
|
||||
CREATE_INDEX_INSPECTIONS_STATUS,
|
||||
CREATE_INDEX_INSPECTIONS_CREATED,
|
||||
SETTINGS_GET,
|
||||
@ -58,6 +65,9 @@ const SQLQueries = {
|
||||
INSPECTIONS_DELETE,
|
||||
INSPECTIONS_DELETE_ALL,
|
||||
INSPECTIONS_COUNT,
|
||||
AUTH_SESSION_SET,
|
||||
AUTH_SESSION_GET,
|
||||
AUTH_SESSION_CLEAR,
|
||||
UTILITY_CHECK_TABLE,
|
||||
UTILITY_DROP_TABLE,
|
||||
UTILITY_VACUUM
|
||||
|
||||
19
rn/app/src/database/sql/auth/clear_auth_session.sql.js
Normal file
19
rn/app/src/database/sql/auth/clear_auth_session.sql.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: очистка сессии авторизации
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const AUTH_SESSION_CLEAR = `
|
||||
-- Удаление сессии авторизации
|
||||
DELETE FROM auth_session WHERE id = 1;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = AUTH_SESSION_CLEAR;
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: создание таблицы сессии авторизации
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const CREATE_TABLE_AUTH_SESSION = `
|
||||
-- Таблица для хранения данных сессии авторизации
|
||||
CREATE TABLE IF NOT EXISTS auth_session (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
session_id TEXT NOT NULL,
|
||||
server_url TEXT NOT NULL,
|
||||
user_rn TEXT,
|
||||
user_code TEXT,
|
||||
user_name TEXT,
|
||||
company_rn TEXT,
|
||||
company_name TEXT,
|
||||
save_password INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CREATE_TABLE_AUTH_SESSION;
|
||||
31
rn/app/src/database/sql/auth/get_auth_session.sql.js
Normal file
31
rn/app/src/database/sql/auth/get_auth_session.sql.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: получение сессии авторизации
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const AUTH_SESSION_GET = `
|
||||
-- Получение сессии авторизации
|
||||
SELECT
|
||||
session_id,
|
||||
server_url,
|
||||
user_rn,
|
||||
user_code,
|
||||
user_name,
|
||||
company_rn,
|
||||
company_name,
|
||||
save_password,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM auth_session
|
||||
WHERE id = 1;
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = AUTH_SESSION_GET;
|
||||
31
rn/app/src/database/sql/auth/set_auth_session.sql.js
Normal file
31
rn/app/src/database/sql/auth/set_auth_session.sql.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
SQL запрос: сохранение или обновление сессии авторизации
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const AUTH_SESSION_SET = `
|
||||
-- Сохранение или обновление сессии авторизации
|
||||
INSERT OR REPLACE INTO auth_session (
|
||||
id,
|
||||
session_id,
|
||||
server_url,
|
||||
user_rn,
|
||||
user_code,
|
||||
user_name,
|
||||
company_rn,
|
||||
company_name,
|
||||
save_password,
|
||||
updated_at
|
||||
)
|
||||
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP);
|
||||
`;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = AUTH_SESSION_SET;
|
||||
@ -23,4 +23,4 @@ CREATE TABLE IF NOT EXISTS inspections (
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = CREATE_TABLE_INSPECTIONS;
|
||||
module.exports = CREATE_TABLE_INSPECTIONS;
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { APP_MESSAGE_VARIANT } = require('../components/common/AppMessage'); //Константы сообщений
|
||||
const { APP_MESSAGE_VARIANT, MSG_AT } = require('../config/messagingConfig'); //Типы сообщений и действий
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
@ -28,7 +28,7 @@ const INITIAL_STATE = {
|
||||
headerStyle: null
|
||||
};
|
||||
|
||||
//Типы сообщений
|
||||
//Типы сообщений (алиасы для вариантов)
|
||||
const MSG_TYPE = {
|
||||
INFO: APP_MESSAGE_VARIANT.INFO,
|
||||
WARN: APP_MESSAGE_VARIANT.WARN,
|
||||
@ -36,12 +36,6 @@ const MSG_TYPE = {
|
||||
SUCCESS: APP_MESSAGE_VARIANT.SUCCESS
|
||||
};
|
||||
|
||||
//Типы действий
|
||||
const MSG_AT = {
|
||||
SHOW_MSG: 'SHOW_MSG',
|
||||
HIDE_MSG: 'HIDE_MSG'
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
@ -8,16 +8,7 @@
|
||||
//---------------------
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Экраны приложения
|
||||
const SCREENS = {
|
||||
MAIN: 'MAIN',
|
||||
SETTINGS: 'SETTINGS'
|
||||
};
|
||||
const { SCREENS } = require('../config/routes'); //Экраны навигации
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -25,10 +16,11 @@ const SCREENS = {
|
||||
|
||||
//Хук навигации приложения
|
||||
const useAppNavigation = () => {
|
||||
//Начальный экран - AUTH (до определения статуса авторизации)
|
||||
const [navigationState, setNavigationState] = React.useState({
|
||||
currentScreen: SCREENS.MAIN,
|
||||
currentScreen: SCREENS.AUTH,
|
||||
screenParams: {},
|
||||
history: [SCREENS.MAIN]
|
||||
history: [SCREENS.AUTH]
|
||||
});
|
||||
|
||||
//Навигация на экран
|
||||
@ -68,6 +60,15 @@ const useAppNavigation = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
//Установка начального экрана (без добавления в историю)
|
||||
const setInitialScreen = React.useCallback((screen, params = {}) => {
|
||||
setNavigationState({
|
||||
currentScreen: screen,
|
||||
screenParams: params,
|
||||
history: [screen]
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
SCREENS,
|
||||
currentScreen: navigationState.currentScreen,
|
||||
@ -75,6 +76,7 @@ const useAppNavigation = () => {
|
||||
navigate,
|
||||
goBack,
|
||||
reset,
|
||||
setInitialScreen,
|
||||
canGoBack: navigationState.history.length > 1
|
||||
};
|
||||
};
|
||||
|
||||
628
rn/app/src/hooks/useAuth.js
Normal file
628
rn/app/src/hooks/useAuth.js
Normal file
@ -0,0 +1,628 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Хук для управления авторизацией
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider');
|
||||
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig');
|
||||
const { ACTION_CODES, RESPONSE_STATES, ERROR_MESSAGES } = require('../config/authApi');
|
||||
const { generateSecretKey, encryptData, decryptData } = require('../utils/secureStorage');
|
||||
const { getPersistentDeviceId, isPersistentIdAvailable } = require('../utils/deviceId');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Хук для управления авторизацией
|
||||
function useAuth() {
|
||||
const { getSetting, setSetting, getAuthSession, setAuthSession, clearAuthSession, isDbReady } = useAppLocalDbContext();
|
||||
|
||||
//Состояние авторизации
|
||||
const [session, setSession] = React.useState(null);
|
||||
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
//Ссылка для контроллера отмены запросов
|
||||
const abortControllerRef = React.useRef(null);
|
||||
|
||||
//Флаг: сессия восстановлена из хранилища при открытии приложения
|
||||
const sessionRestoredFromStorageRef = React.useRef(false);
|
||||
|
||||
//Загрузка или получение постоянного идентификатора устройства
|
||||
const getDeviceId = React.useCallback(async () => {
|
||||
//Сначала проверяем сохранённый идентификатор
|
||||
let deviceId = await getSetting(AUTH_SETTINGS_KEYS.DEVICE_ID);
|
||||
|
||||
if (deviceId) {
|
||||
//Проверяем доступность постоянного идентификатора
|
||||
const persistentAvailable = await isPersistentIdAvailable();
|
||||
|
||||
if (persistentAvailable) {
|
||||
//Получаем постоянный идентификатор для сравнения
|
||||
const persistentId = await getPersistentDeviceId();
|
||||
|
||||
//Если постоянный ID отличается - обновляем на постоянный
|
||||
if (persistentId && !deviceId.includes('-') && deviceId !== persistentId) {
|
||||
deviceId = persistentId;
|
||||
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_ID, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
//Генерируем новый идентификатор
|
||||
deviceId = await getPersistentDeviceId();
|
||||
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_ID, deviceId);
|
||||
|
||||
return deviceId;
|
||||
}, [getSetting, setSetting]);
|
||||
|
||||
//Загрузка или генерация уникального секретного ключа устройства
|
||||
const getSecretKey = React.useCallback(async () => {
|
||||
let secretKey = await getSetting(AUTH_SETTINGS_KEYS.DEVICE_SECRET_KEY);
|
||||
|
||||
if (!secretKey) {
|
||||
secretKey = generateSecretKey();
|
||||
await setSetting(AUTH_SETTINGS_KEYS.DEVICE_SECRET_KEY, secretKey);
|
||||
}
|
||||
|
||||
return secretKey;
|
||||
}, [getSetting, setSetting]);
|
||||
|
||||
//Выполнение запроса к серверу
|
||||
const executeRequest = React.useCallback(async (serverUrl, payload) => {
|
||||
//Отменяем предыдущий запрос если есть
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
const response = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${ERROR_MESSAGES.SERVER_ERROR}: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (fetchError) {
|
||||
if (fetchError.name === 'AbortError') {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR}: ${fetchError.message}`);
|
||||
} finally {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
//Сохранение credentials после успешной аутентификации
|
||||
const saveCredentials = React.useCallback(
|
||||
async (login, password, deviceId) => {
|
||||
try {
|
||||
//Получаем секретный ключ устройства
|
||||
const secretKey = await getSecretKey();
|
||||
|
||||
//Шифруем пароль используя секретный ключ и deviceId как соль
|
||||
const encryptedPassword = encryptData(password, secretKey, deviceId || '');
|
||||
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, login);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, encryptedPassword);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'true');
|
||||
|
||||
return true;
|
||||
} catch (saveError) {
|
||||
console.error('Ошибка сохранения credentials:', saveError);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[setSetting, getSecretKey]
|
||||
);
|
||||
|
||||
//Загрузка сохранённых credentials
|
||||
const getSavedCredentials = React.useCallback(async () => {
|
||||
try {
|
||||
const savedLogin = await getSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN);
|
||||
|
||||
//Если нет сохранённого логина - возвращаем null
|
||||
if (!savedLogin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const savePasswordEnabled = (await getSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED)) === 'true';
|
||||
|
||||
//Если пароль не сохранялся - возвращаем только логин
|
||||
if (!savePasswordEnabled) {
|
||||
return {
|
||||
login: savedLogin,
|
||||
password: null,
|
||||
savePasswordEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
const savedPassword = await getSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD);
|
||||
|
||||
if (!savedPassword) {
|
||||
return {
|
||||
login: savedLogin,
|
||||
password: null,
|
||||
savePasswordEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
//Получаем deviceId и секретный ключ для расшифровки
|
||||
const deviceId = await getDeviceId();
|
||||
const secretKey = await getSecretKey();
|
||||
|
||||
//Расшифровываем пароль
|
||||
const decryptedPassword = decryptData(savedPassword, secretKey, deviceId || '');
|
||||
|
||||
return {
|
||||
login: savedLogin,
|
||||
password: decryptedPassword,
|
||||
savePasswordEnabled: true
|
||||
};
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки credentials:', loadError);
|
||||
return null;
|
||||
}
|
||||
}, [getSetting, getDeviceId, getSecretKey]);
|
||||
|
||||
//Очистка сохранённых credentials
|
||||
const clearSavedCredentials = React.useCallback(async () => {
|
||||
try {
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, '');
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_PASSWORD, '');
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVE_PASSWORD_ENABLED, 'false');
|
||||
return true;
|
||||
} catch (clearError) {
|
||||
console.error('Ошибка очистки credentials:', clearError);
|
||||
return false;
|
||||
}
|
||||
}, [setSetting]);
|
||||
|
||||
//Вход в систему
|
||||
const login = React.useCallback(
|
||||
async ({ serverUrl, company, user, password, timeout, savePassword = false }) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
//Получаем идентификатор устройства
|
||||
const deviceId = await getDeviceId();
|
||||
|
||||
//Формируем запрос
|
||||
const requestPayload = {
|
||||
XREQUEST: {
|
||||
XACTION: {
|
||||
SCODE: ACTION_CODES.LOG_IN
|
||||
},
|
||||
XPAYLOAD: {
|
||||
SUSER: user,
|
||||
SPASSWORD: password,
|
||||
STERMINAL: deviceId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Добавляем организацию если указана
|
||||
if (company) {
|
||||
requestPayload.XREQUEST.XPAYLOAD.SCOMPANY = company;
|
||||
}
|
||||
|
||||
//Добавляем таймаут (используем значение по умолчанию если не указан)
|
||||
const timeoutValue = timeout && timeout > 0 ? timeout : DEFAULT_IDLE_TIMEOUT;
|
||||
requestPayload.XREQUEST.XPAYLOAD.NTIMEOUT = timeoutValue;
|
||||
|
||||
//Выполняем запрос
|
||||
const response = await executeRequest(serverUrl, requestPayload);
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: ERROR_MESSAGES.NETWORK_ERROR };
|
||||
}
|
||||
|
||||
//Проверяем ответ
|
||||
if (!response.XRESPONSE) {
|
||||
return { success: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
||||
}
|
||||
|
||||
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
||||
|
||||
if (SSTATE !== RESPONSE_STATES.OK) {
|
||||
return { success: false, error: XPAYLOAD?.SERROR || ERROR_MESSAGES.LOGIN_FAILED };
|
||||
}
|
||||
|
||||
//Проверяем наличие списка организаций (требуется выбор)
|
||||
if (XPAYLOAD.XCOMPANIES && Array.isArray(XPAYLOAD.XCOMPANIES) && XPAYLOAD.XCOMPANIES.length > 0 && !XPAYLOAD.XCOMPANY) {
|
||||
//Преобразуем массив организаций
|
||||
const organizations = XPAYLOAD.XCOMPANIES.map(item => item.XCOMPANY || item);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
needSelectCompany: true,
|
||||
sessionId: XPAYLOAD.SSESSION,
|
||||
user: XPAYLOAD.XUSER,
|
||||
organizations,
|
||||
serverUrl,
|
||||
savePassword,
|
||||
//Передаём данные для сохранения credentials после выбора организации
|
||||
loginCredentials: {
|
||||
login: user,
|
||||
password,
|
||||
deviceId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//Сохраняем сессию
|
||||
const sessionData = {
|
||||
sessionId: XPAYLOAD.SSESSION,
|
||||
serverUrl,
|
||||
userRn: XPAYLOAD.XUSER?.NRN,
|
||||
userCode: XPAYLOAD.XUSER?.SCODE,
|
||||
userName: XPAYLOAD.XUSER?.SNAME,
|
||||
companyRn: XPAYLOAD.XCOMPANY?.NRN,
|
||||
companyName: XPAYLOAD.XCOMPANY?.SNAME,
|
||||
savePassword
|
||||
};
|
||||
|
||||
await setAuthSession(sessionData);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, user);
|
||||
|
||||
if (savePassword) {
|
||||
await saveCredentials(user, password, deviceId);
|
||||
}
|
||||
|
||||
setSession(sessionData);
|
||||
setIsAuthenticated(true);
|
||||
sessionRestoredFromStorageRef.current = false;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
needSelectCompany: false,
|
||||
session: sessionData
|
||||
};
|
||||
} catch (loginError) {
|
||||
const errorMessage = loginError.message || ERROR_MESSAGES.LOGIN_FAILED;
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[executeRequest, getDeviceId, setAuthSession, setSetting, saveCredentials]
|
||||
);
|
||||
|
||||
//Выбор организации после входа (SET_COMPANY)
|
||||
const selectCompany = React.useCallback(
|
||||
async ({ serverUrl, sessionId, user, company, savePassword, loginCredentials }) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
//Формируем запрос SET_COMPANY
|
||||
const requestPayload = {
|
||||
XREQUEST: {
|
||||
XACTION: {
|
||||
SCODE: ACTION_CODES.SET_COMPANY
|
||||
},
|
||||
XPAYLOAD: {
|
||||
SSESSION: sessionId,
|
||||
NCOMPANY: company.NRN,
|
||||
SCOMPANY: company.SNAME
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Выполняем запрос
|
||||
const response = await executeRequest(serverUrl, requestPayload);
|
||||
|
||||
if (!response) {
|
||||
return { success: false, error: ERROR_MESSAGES.NETWORK_ERROR };
|
||||
}
|
||||
|
||||
//Проверяем ответ
|
||||
if (!response.XRESPONSE) {
|
||||
return { success: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
||||
}
|
||||
|
||||
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
||||
|
||||
if (SSTATE !== RESPONSE_STATES.OK) {
|
||||
return { success: false, error: XPAYLOAD?.SERROR || 'Ошибка установки организации' };
|
||||
}
|
||||
|
||||
//Сохраняем сессию
|
||||
const sessionData = {
|
||||
sessionId,
|
||||
serverUrl,
|
||||
userRn: user?.NRN,
|
||||
userCode: user?.SCODE,
|
||||
userName: user?.SNAME,
|
||||
companyRn: XPAYLOAD.XCOMPANY?.NRN || company.NRN,
|
||||
companyName: XPAYLOAD.XCOMPANY?.SNAME || company.SNAME,
|
||||
savePassword
|
||||
};
|
||||
|
||||
await setAuthSession(sessionData);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, serverUrl);
|
||||
const loginToSave = loginCredentials?.login || user?.SCODE || user?.SNAME || '';
|
||||
if (loginToSave) {
|
||||
await setSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN, loginToSave);
|
||||
}
|
||||
|
||||
if (savePassword && loginCredentials) {
|
||||
await saveCredentials(loginCredentials.login, loginCredentials.password, loginCredentials.deviceId);
|
||||
}
|
||||
|
||||
setSession(sessionData);
|
||||
setIsAuthenticated(true);
|
||||
sessionRestoredFromStorageRef.current = false;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session: sessionData
|
||||
};
|
||||
} catch (selectError) {
|
||||
const errorMessage = selectError.message || 'Ошибка установки организации';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[executeRequest, setAuthSession, setSetting, saveCredentials]
|
||||
);
|
||||
|
||||
//Выход из системы
|
||||
const logout = React.useCallback(
|
||||
async (options = {}) => {
|
||||
const { skipServerRequest = false } = options;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const currentSession = session || (await getAuthSession());
|
||||
|
||||
//Запрос на сервер только при онлайн и если не отключено опцией
|
||||
if (!skipServerRequest && currentSession?.sessionId && currentSession?.serverUrl) {
|
||||
const requestPayload = {
|
||||
XREQUEST: {
|
||||
XACTION: {
|
||||
SCODE: ACTION_CODES.LOG_OUT
|
||||
},
|
||||
XPAYLOAD: {
|
||||
SSESSION: currentSession.sessionId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await executeRequest(currentSession.serverUrl, requestPayload);
|
||||
} catch (logoutError) {
|
||||
console.warn('Ошибка при выходе из системы:', logoutError);
|
||||
}
|
||||
}
|
||||
|
||||
await clearAuthSession();
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
return { success: true };
|
||||
} catch (logoutError) {
|
||||
const errorMessage = logoutError.message || ERROR_MESSAGES.LOGOUT_FAILED;
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[session, getAuthSession, executeRequest, clearAuthSession]
|
||||
);
|
||||
|
||||
//Проверка сессии
|
||||
const checkSession = React.useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
//Получаем сохраненную сессию
|
||||
const savedSession = await getAuthSession();
|
||||
|
||||
if (!savedSession?.sessionId || !savedSession?.serverUrl) {
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
return { success: false, isOffline: false, error: null };
|
||||
}
|
||||
|
||||
//Формируем запрос
|
||||
const requestPayload = {
|
||||
XREQUEST: {
|
||||
XACTION: {
|
||||
SCODE: ACTION_CODES.CHECK_SESSION
|
||||
},
|
||||
XPAYLOAD: {
|
||||
SSESSION: savedSession.sessionId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Выполняем запрос
|
||||
let response = null;
|
||||
try {
|
||||
response = await executeRequest(savedSession.serverUrl, requestPayload);
|
||||
} catch (requestError) {
|
||||
const isNetworkError = requestError.message && String(requestError.message).indexOf(ERROR_MESSAGES.NETWORK_ERROR) !== -1;
|
||||
if (isNetworkError) {
|
||||
setSession(savedSession);
|
||||
setIsAuthenticated(true);
|
||||
return { success: true, isOffline: true, session: savedSession };
|
||||
}
|
||||
throw requestError;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
setSession(savedSession);
|
||||
setIsAuthenticated(true);
|
||||
return { success: true, isOffline: true, session: savedSession };
|
||||
}
|
||||
|
||||
//Проверяем ответ: если сервер ответил, но структура некорректна — не считаем оффлайн
|
||||
if (!response.XRESPONSE) {
|
||||
return { success: false, isOffline: false, error: ERROR_MESSAGES.INVALID_RESPONSE };
|
||||
}
|
||||
|
||||
const { SSTATE, XPAYLOAD } = response.XRESPONSE;
|
||||
|
||||
if (SSTATE !== RESPONSE_STATES.OK) {
|
||||
//Сессия недействительна - очищаем данные
|
||||
await clearAuthSession();
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
return { success: false, isOffline: false, error: ERROR_MESSAGES.SESSION_EXPIRED };
|
||||
}
|
||||
|
||||
//Проверяем необходимость выбора организации
|
||||
if (XPAYLOAD.XCOMPANIES && !XPAYLOAD.XCOMPANY) {
|
||||
const organizations = XPAYLOAD.XCOMPANIES.map(item => item.XCOMPANY || item);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
isOffline: false,
|
||||
needSelectCompany: true,
|
||||
sessionId: XPAYLOAD.SSESSION || savedSession.sessionId,
|
||||
user: XPAYLOAD.XUSER,
|
||||
organizations,
|
||||
serverUrl: savedSession.serverUrl,
|
||||
savePassword: savedSession.savePassword
|
||||
};
|
||||
}
|
||||
|
||||
//Обновляем данные сессии
|
||||
const updatedSession = {
|
||||
sessionId: XPAYLOAD.SSESSION || savedSession.sessionId,
|
||||
serverUrl: savedSession.serverUrl,
|
||||
userRn: XPAYLOAD.XUSER?.NRN || savedSession.userRn,
|
||||
userCode: XPAYLOAD.XUSER?.SCODE || savedSession.userCode,
|
||||
userName: XPAYLOAD.XUSER?.SNAME || savedSession.userName,
|
||||
companyRn: XPAYLOAD.XCOMPANY?.NRN || savedSession.companyRn,
|
||||
companyName: XPAYLOAD.XCOMPANY?.SNAME || savedSession.companyName,
|
||||
savePassword: savedSession.savePassword
|
||||
};
|
||||
|
||||
await setAuthSession(updatedSession);
|
||||
setSession(updatedSession);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
return { success: true, isOffline: false, session: updatedSession };
|
||||
} catch (checkError) {
|
||||
//Оффлайн только при ошибке сети; при прочих ошибках — не переключаем в оффлайн
|
||||
const isNetworkError = checkError.message && String(checkError.message).indexOf(ERROR_MESSAGES.NETWORK_ERROR) !== -1;
|
||||
const savedSession = await getAuthSession();
|
||||
|
||||
if (isNetworkError && savedSession?.sessionId) {
|
||||
setSession(savedSession);
|
||||
setIsAuthenticated(true);
|
||||
return { success: true, isOffline: true, session: savedSession };
|
||||
}
|
||||
|
||||
setSession(null);
|
||||
setIsAuthenticated(false);
|
||||
return { success: false, isOffline: false, error: checkError.message };
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [getAuthSession, executeRequest, clearAuthSession, setAuthSession]);
|
||||
|
||||
//Инициализация при готовности БД
|
||||
React.useEffect(() => {
|
||||
if (!isDbReady || isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
const savedSession = await getAuthSession();
|
||||
|
||||
if (savedSession?.sessionId) {
|
||||
setSession(savedSession);
|
||||
setIsAuthenticated(true);
|
||||
sessionRestoredFromStorageRef.current = true;
|
||||
}
|
||||
} catch (initError) {
|
||||
console.error('Ошибка инициализации авторизации:', initError);
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
initAuth();
|
||||
}, [isDbReady, isInitialized, getAuthSession]);
|
||||
|
||||
//Получить и сбросить флаг «сессия восстановлена при открытии» (для проверки соединения только при холодном старте)
|
||||
const getAndClearSessionRestoredFromStorage = React.useCallback(() => {
|
||||
const value = sessionRestoredFromStorageRef.current;
|
||||
sessionRestoredFromStorageRef.current = false;
|
||||
return value;
|
||||
}, []);
|
||||
|
||||
//Отмена запросов при размонтировании
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
//Состояние
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
isInitialized,
|
||||
error,
|
||||
|
||||
//Методы
|
||||
login,
|
||||
logout,
|
||||
selectCompany,
|
||||
checkSession,
|
||||
getDeviceId,
|
||||
getSavedCredentials,
|
||||
clearSavedCredentials,
|
||||
getAuthSession,
|
||||
clearAuthSession,
|
||||
getAndClearSessionRestoredFromStorage,
|
||||
|
||||
//Константы
|
||||
AUTH_SETTINGS_KEYS
|
||||
};
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = useAuth;
|
||||
@ -15,7 +15,15 @@ const { BackHandler, Platform } = require('react-native'); //BackHandler и пл
|
||||
//-----------
|
||||
|
||||
//Хук обработки аппаратной кнопки "Назад"
|
||||
const useHardwareBackPress = (handler, deps = []) => {
|
||||
const useHardwareBackPress = handler => {
|
||||
//Ref для хранения актуального обработчика
|
||||
const handlerRef = React.useRef(handler);
|
||||
|
||||
//Обновление ref при изменении обработчика
|
||||
React.useEffect(() => {
|
||||
handlerRef.current = handler;
|
||||
}, [handler]);
|
||||
|
||||
React.useEffect(() => {
|
||||
//BackHandler работает только на Android
|
||||
if (Platform.OS !== 'android') {
|
||||
@ -24,8 +32,8 @@ const useHardwareBackPress = (handler, deps = []) => {
|
||||
|
||||
//Обработчик нажатия кнопки "Назад"
|
||||
const backHandler = () => {
|
||||
if (typeof handler === 'function') {
|
||||
return handler();
|
||||
if (typeof handlerRef.current === 'function') {
|
||||
return handlerRef.current();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@ -37,8 +45,7 @@ const useHardwareBackPress = (handler, deps = []) => {
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}, []);
|
||||
};
|
||||
|
||||
//----------------
|
||||
|
||||
@ -250,6 +250,54 @@ function useLocalDb() {
|
||||
[isDbReady]
|
||||
);
|
||||
|
||||
//Сохранение сессии авторизации
|
||||
const setAuthSession = React.useCallback(
|
||||
async session => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.setAuthSession(session);
|
||||
} catch (setAuthSessionError) {
|
||||
console.error('Ошибка сохранения сессии авторизации:', setAuthSessionError);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[isDbReady]
|
||||
);
|
||||
|
||||
//Получение сессии авторизации
|
||||
const getAuthSession = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.getAuthSession();
|
||||
} catch (getAuthSessionError) {
|
||||
console.error('Ошибка получения сессии авторизации:', getAuthSessionError);
|
||||
return null;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
//Очистка сессии авторизации
|
||||
const clearAuthSession = React.useCallback(async () => {
|
||||
if (!isDbReady) {
|
||||
console.warn('База данных не готова');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await SQLiteDatabase.clearAuthSession();
|
||||
} catch (clearAuthSessionError) {
|
||||
console.error('Ошибка очистки сессии авторизации:', clearAuthSessionError);
|
||||
return false;
|
||||
}
|
||||
}, [isDbReady]);
|
||||
|
||||
return {
|
||||
isDbReady,
|
||||
inspections,
|
||||
@ -263,7 +311,10 @@ function useLocalDb() {
|
||||
clearSettings,
|
||||
clearInspections,
|
||||
vacuum,
|
||||
checkTableExists
|
||||
checkTableExists,
|
||||
setAuthSession,
|
||||
getAuthSession,
|
||||
clearAuthSession
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -15,17 +15,8 @@
|
||||
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';
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
||||
const { LOAD_STATUS_IDLE, LOAD_STATUS_LOADING, LOAD_STATUS_DONE, LOAD_STATUS_ERROR } = require('../config/loadStatus'); //Статусы загрузки
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -35,7 +26,7 @@ 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 { APP_MODE, mode } = useAppModeContext();
|
||||
|
||||
const [loadStatus, setLoadStatus] = React.useState(LOAD_STATUS_IDLE);
|
||||
const [error, setError] = React.useState(null);
|
||||
@ -73,7 +64,7 @@ function usePreTripInspections() {
|
||||
payload: {}
|
||||
});
|
||||
|
||||
//Ошибка сервера приложений - пробуем взять данные из локальной БД
|
||||
//Ошибка запроса — используем локальные данные
|
||||
if (isRespErr(serverResponse) || serverResponse.status !== RESP_STATUS_OK) {
|
||||
const localInspections = await loadInspections();
|
||||
setLoadStatus(localInspections.length > 0 ? LOAD_STATUS_DONE : LOAD_STATUS_ERROR);
|
||||
@ -110,7 +101,6 @@ function usePreTripInspections() {
|
||||
await saveInspection(safeInspection);
|
||||
|
||||
//Если приложение в режиме ONLINE - отправляем данные на сервер приложений
|
||||
//TODO: вызов конкретного метода сервера
|
||||
if (mode === APP_MODE.ONLINE) {
|
||||
await executeAction({
|
||||
path: 'api/pretrip/inspections/save',
|
||||
@ -138,10 +128,4 @@ function usePreTripInspections() {
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
usePreTripInspections,
|
||||
LOAD_STATUS_IDLE,
|
||||
LOAD_STATUS_LOADING,
|
||||
LOAD_STATUS_DONE,
|
||||
LOAD_STATUS_ERROR
|
||||
};
|
||||
module.exports = usePreTripInspections;
|
||||
|
||||
604
rn/app/src/screens/AuthScreen.js
Normal file
604
rn/app/src/screens/AuthScreen.js
Normal file
@ -0,0 +1,604 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Экран аутентификации
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const React = require('react');
|
||||
const { ScrollView, View, KeyboardAvoidingView, Platform } = require('react-native');
|
||||
const AdaptiveView = require('../components/common/AdaptiveView');
|
||||
const AppText = require('../components/common/AppText');
|
||||
const AppInput = require('../components/common/AppInput');
|
||||
const AppButton = require('../components/common/AppButton');
|
||||
const AppSwitch = require('../components/common/AppSwitch');
|
||||
const PasswordInput = require('../components/common/PasswordInput');
|
||||
const AppHeader = require('../components/layout/AppHeader');
|
||||
const AppLogo = require('../components/common/AppLogo');
|
||||
const SideMenu = require('../components/menu/SideMenu');
|
||||
const LoadingOverlay = require('../components/common/LoadingOverlay');
|
||||
const OrganizationSelectDialog = require('../components/auth/OrganizationSelectDialog');
|
||||
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 { useAppAuthContext } = require('../components/layout/AppAuthProvider');
|
||||
const { getAppInfo } = require('../utils/appInfo');
|
||||
const { isServerUrlFieldVisible } = require('../utils/loginFormUtils');
|
||||
const { normalizeServerUrl, validateServerUrl } = require('../utils/validation');
|
||||
const { AUTH_SETTINGS_KEYS } = require('../config/authConfig');
|
||||
const styles = require('../styles/screens/AuthScreen.styles');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Экран аутентификации
|
||||
function AuthScreen() {
|
||||
const { showError, showSuccess, showInfo } = useAppMessagingContext();
|
||||
const { APP_MODE, mode, setOnline } = useAppModeContext();
|
||||
const { navigate, goBack, canGoBack, reset, screenParams, SCREENS } = useAppNavigationContext();
|
||||
const { getSetting, isDbReady, clearInspections } = useAppLocalDbContext();
|
||||
const {
|
||||
session,
|
||||
login,
|
||||
selectCompany,
|
||||
isLoading,
|
||||
getSavedCredentials,
|
||||
getAuthSession,
|
||||
clearAuthSession,
|
||||
authFormData,
|
||||
updateAuthFormData,
|
||||
clearAuthFormData
|
||||
} = useAppAuthContext();
|
||||
|
||||
//Состояние меню
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
|
||||
//Состояние формы
|
||||
const [serverUrl, setServerUrl] = React.useState(authFormData.serverUrl);
|
||||
const [username, setUsername] = React.useState(authFormData.username);
|
||||
const [password, setPassword] = React.useState(authFormData.password);
|
||||
const [savePassword, setSavePassword] = React.useState(authFormData.savePassword);
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
//Состояние отображения
|
||||
const [hideServerUrl, setHideServerUrl] = React.useState(false);
|
||||
const [isSettingsLoaded, setIsSettingsLoaded] = React.useState(false);
|
||||
|
||||
//Флаг для предотвращения повторной загрузки
|
||||
const initialLoadRef = React.useRef(false);
|
||||
|
||||
//Флаг однократной подстановки сохранённого логина при готовности БД
|
||||
const savedLoginFilledRef = React.useRef(false);
|
||||
|
||||
//Состояние выбора организации
|
||||
const [showOrgDialog, setShowOrgDialog] = React.useState(false);
|
||||
const [organizations, setOrganizations] = React.useState([]);
|
||||
const [pendingLoginData, setPendingLoginData] = React.useState(null);
|
||||
|
||||
//Определяем режим открытия экрана (из меню или при старте)
|
||||
const isFromMenu = screenParams?.fromMenu === true;
|
||||
const fromMenuKey = screenParams?.fromMenuKey;
|
||||
|
||||
//Ref для хранения актуальных значений формы (для синхронизации при размонтировании)
|
||||
const formDataRef = React.useRef({ serverUrl, username, password, savePassword });
|
||||
|
||||
//Ref полей ввода для перехода фокуса по Enter
|
||||
const serverInputRef = React.useRef(null);
|
||||
const loginInputRef = React.useRef(null);
|
||||
const passwordInputRef = React.useRef(null);
|
||||
|
||||
//Обновление ref при изменении значений формы
|
||||
React.useEffect(() => {
|
||||
formDataRef.current = { serverUrl, username, password, savePassword };
|
||||
}, [serverUrl, username, password, savePassword]);
|
||||
|
||||
//Синхронизация данных формы с контекстом при размонтировании компонента
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
updateAuthFormData(formDataRef.current);
|
||||
};
|
||||
}, [updateAuthFormData]);
|
||||
|
||||
//При готовности БД один раз подставляем сохранённый логин в поле (если оно пустое)
|
||||
React.useEffect(() => {
|
||||
if (!isDbReady || savedLoginFilledRef.current) {
|
||||
return;
|
||||
}
|
||||
savedLoginFilledRef.current = true;
|
||||
let cancelled = false;
|
||||
getSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN)
|
||||
.then(val => {
|
||||
if (!cancelled && val && typeof val === 'string' && val.trim()) {
|
||||
setUsername(val.trim());
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isDbReady, getSetting]);
|
||||
|
||||
//Сброс формы и загрузка credentials при переходе по кнопке "Вход" из меню или при открытии экрана в оффлайн
|
||||
//fromMenuKey в deps обеспечивает повторную загрузку при каждом новом переходе из меню
|
||||
React.useEffect(() => {
|
||||
const shouldLoadFromMenu = isFromMenu && isDbReady;
|
||||
const shouldLoadOffline = mode === APP_MODE.OFFLINE && isDbReady;
|
||||
if (!shouldLoadFromMenu && !shouldLoadOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadCredentialsFromMenu = async () => {
|
||||
try {
|
||||
if (isFromMenu) {
|
||||
clearAuthFormData();
|
||||
}
|
||||
|
||||
//Загружаем сохранённый URL сервера
|
||||
const savedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL);
|
||||
const savedHideServerUrl = await getSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL);
|
||||
|
||||
if (savedServerUrl) {
|
||||
setServerUrl(savedServerUrl);
|
||||
}
|
||||
|
||||
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
||||
|
||||
//Загружаем сохранённые credentials
|
||||
const savedCredentials = await getSavedCredentials();
|
||||
|
||||
if (savedCredentials && savedCredentials.login) {
|
||||
//Логин подставляем всегда
|
||||
setUsername(savedCredentials.login);
|
||||
|
||||
//Пароль подставляем только если он был сохранён
|
||||
if (savedCredentials.savePasswordEnabled && savedCredentials.password) {
|
||||
setPassword(savedCredentials.password);
|
||||
setSavePassword(true);
|
||||
} else {
|
||||
//Сбрасываем пароль
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
}
|
||||
} else {
|
||||
//Нет сохранённых данных - сбрасываем форму
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
}
|
||||
|
||||
initialLoadRef.current = true;
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки credentials из меню:', loadError);
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
} finally {
|
||||
setIsSettingsLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadCredentialsFromMenu();
|
||||
}, [isFromMenu, isDbReady, fromMenuKey, mode, APP_MODE.OFFLINE, clearAuthFormData, getSetting, getSavedCredentials]);
|
||||
|
||||
//Резервная загрузка только логина (если поле пустое) при открытии в оффлайн или из меню
|
||||
React.useEffect(() => {
|
||||
if (!isDbReady || username !== '' || (!isFromMenu && mode !== APP_MODE.OFFLINE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadSavedLoginOnly = async () => {
|
||||
try {
|
||||
const savedLogin = await getSetting(AUTH_SETTINGS_KEYS.SAVED_LOGIN);
|
||||
if (savedLogin && typeof savedLogin === 'string') {
|
||||
setUsername(savedLogin.trim());
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Резервная загрузка логина:', e);
|
||||
}
|
||||
};
|
||||
|
||||
loadSavedLoginOnly();
|
||||
}, [isDbReady, username, isFromMenu, mode, APP_MODE.OFFLINE, getSetting]);
|
||||
|
||||
//Загрузка настроек при готовности БД
|
||||
//В т.ч. при работе в оффлайн подставляем сохранённый логин и при необходимости пароль
|
||||
React.useEffect(() => {
|
||||
//Пропускаем если БД не готова, уже загружали или открыто из меню
|
||||
if (!isDbReady || initialLoadRef.current || isFromMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
initialLoadRef.current = true;
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const savedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL);
|
||||
const savedHideServerUrl = await getSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL);
|
||||
|
||||
//Получаем текущие значения формы
|
||||
const currentFormData = formDataRef.current;
|
||||
|
||||
if (savedServerUrl && !currentFormData.serverUrl) {
|
||||
setServerUrl(savedServerUrl);
|
||||
}
|
||||
|
||||
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
||||
|
||||
//Загружаем сохранённые credentials (логин/пароль) для отображения в т.ч. в оффлайн
|
||||
const savedCredentials = await getSavedCredentials();
|
||||
if (savedCredentials && savedCredentials.login) {
|
||||
setUsername(savedCredentials.login);
|
||||
if (savedCredentials.savePasswordEnabled && savedCredentials.password) {
|
||||
setPassword(savedCredentials.password);
|
||||
setSavePassword(true);
|
||||
} else {
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
}
|
||||
} else {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
setSavePassword(false);
|
||||
}
|
||||
} catch (loadError) {
|
||||
console.error('Ошибка загрузки настроек авторизации:', loadError);
|
||||
} finally {
|
||||
setIsSettingsLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [isDbReady, isFromMenu, getSetting, getSavedCredentials]);
|
||||
|
||||
//Валидация формы
|
||||
const validateForm = React.useCallback(() => {
|
||||
if (!hideServerUrl || !serverUrl) {
|
||||
const urlResult = validateServerUrl(serverUrl, { emptyMessage: 'Укажите адрес сервера' });
|
||||
if (urlResult !== true) {
|
||||
showError(urlResult);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!username.trim()) {
|
||||
showError('Укажите логин');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
showError('Укажите пароль');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [hideServerUrl, serverUrl, username, password, showError]);
|
||||
|
||||
//Выполнение входа
|
||||
const performLogin = React.useCallback(async () => {
|
||||
let idleTimeout = null;
|
||||
try {
|
||||
idleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
} catch (settingError) {
|
||||
console.warn('Ошибка получения таймаута:', settingError);
|
||||
}
|
||||
|
||||
const result = await login({
|
||||
serverUrl: serverUrl.trim(),
|
||||
user: username.trim(),
|
||||
password: password.trim(),
|
||||
timeout: idleTimeout ? parseInt(idleTimeout, 10) : null,
|
||||
savePassword
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Ошибка входа');
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.needSelectCompany) {
|
||||
setOrganizations(result.organizations);
|
||||
setPendingLoginData({
|
||||
serverUrl: result.serverUrl,
|
||||
sessionId: result.sessionId,
|
||||
user: result.user,
|
||||
savePassword: result.savePassword,
|
||||
loginCredentials: result.loginCredentials
|
||||
});
|
||||
setShowOrgDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccess('Вход выполнен успешно');
|
||||
setOnline();
|
||||
clearAuthFormData();
|
||||
reset();
|
||||
}, [login, serverUrl, username, password, savePassword, getSetting, showError, showSuccess, setOnline, clearAuthFormData, reset]);
|
||||
|
||||
//Обработчик входа: проверка смены параметров подключения, при необходимости — предупреждение и сброс локальных данных
|
||||
const handleLogin = React.useCallback(async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNormalized = normalizeServerUrl(serverUrl);
|
||||
const prevSession = await getAuthSession();
|
||||
const savedServerUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL).catch(() => '');
|
||||
const prevServerUrl = prevSession?.serverUrl || savedServerUrl;
|
||||
const prevNormalized = normalizeServerUrl(prevServerUrl);
|
||||
|
||||
const connectionParamsChanged = prevNormalized !== '' && currentNormalized !== prevNormalized;
|
||||
|
||||
if (connectionParamsChanged) {
|
||||
showInfo('При смене параметров подключения все локальные данные будут сброшены. Продолжить?', {
|
||||
title: 'Подтверждение входа',
|
||||
buttons: [
|
||||
{
|
||||
id: 'cancel',
|
||||
title: 'Отмена',
|
||||
onPress: () => {}
|
||||
},
|
||||
{
|
||||
id: 'confirm',
|
||||
title: 'Продолжить',
|
||||
onPress: async () => {
|
||||
await clearAuthSession();
|
||||
await clearInspections();
|
||||
await performLogin();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
await performLogin();
|
||||
}
|
||||
}, [validateForm, serverUrl, getAuthSession, getSetting, showInfo, clearAuthSession, clearInspections, performLogin]);
|
||||
|
||||
//Обработчик выбора организации
|
||||
const handleSelectOrganization = React.useCallback(
|
||||
async org => {
|
||||
setShowOrgDialog(false);
|
||||
|
||||
if (!pendingLoginData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await selectCompany({
|
||||
serverUrl: pendingLoginData.serverUrl,
|
||||
sessionId: pendingLoginData.sessionId,
|
||||
user: pendingLoginData.user,
|
||||
company: org,
|
||||
savePassword: pendingLoginData.savePassword,
|
||||
loginCredentials: pendingLoginData.loginCredentials
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Ошибка выбора организации');
|
||||
return;
|
||||
}
|
||||
|
||||
//Успешный вход
|
||||
showSuccess('Вход выполнен успешно');
|
||||
setOnline();
|
||||
|
||||
//Очищаем временные данные
|
||||
setPendingLoginData(null);
|
||||
setOrganizations([]);
|
||||
|
||||
//Очищаем данные формы в контексте
|
||||
clearAuthFormData();
|
||||
|
||||
//Сбрасываем навигацию
|
||||
reset();
|
||||
},
|
||||
[pendingLoginData, selectCompany, showError, showSuccess, setOnline, clearAuthFormData, reset]
|
||||
);
|
||||
|
||||
//Обработчик отмены выбора организации
|
||||
const handleCancelOrganization = React.useCallback(() => {
|
||||
setShowOrgDialog(false);
|
||||
setPendingLoginData(null);
|
||||
setOrganizations([]);
|
||||
}, []);
|
||||
|
||||
//Обработчик кнопки назад
|
||||
const handleBackPress = React.useCallback(() => {
|
||||
if (canGoBack) {
|
||||
goBack();
|
||||
}
|
||||
}, [canGoBack, goBack]);
|
||||
|
||||
//Обработчик переключения показа пароля
|
||||
const handleTogglePassword = React.useCallback(value => {
|
||||
setShowPassword(value);
|
||||
}, []);
|
||||
|
||||
//Обработчик переключения сохранения пароля
|
||||
const handleToggleSavePassword = React.useCallback(value => {
|
||||
setSavePassword(value);
|
||||
}, []);
|
||||
|
||||
//При Enter в поле «Сервер» — фокус на логин
|
||||
const handleServerSubmitEditing = React.useCallback(() => {
|
||||
if (loginInputRef.current) {
|
||||
loginInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
//При Enter в поле «Логин» — фокус на пароль
|
||||
const handleLoginSubmitEditing = React.useCallback(() => {
|
||||
if (passwordInputRef.current) {
|
||||
passwordInputRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
//При Enter в поле «Пароль» — нажатие кнопки «Войти»
|
||||
const handlePasswordSubmitEditing = React.useCallback(() => {
|
||||
handleLogin();
|
||||
}, [handleLogin]);
|
||||
|
||||
//Обработчик открытия меню
|
||||
const handleMenuOpen = React.useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
}, []);
|
||||
|
||||
//Обработчик закрытия меню
|
||||
const handleMenuClose = React.useCallback(() => {
|
||||
setMenuVisible(false);
|
||||
}, []);
|
||||
|
||||
//Обработчик перехода в настройки
|
||||
const handleOpenSettings = React.useCallback(() => {
|
||||
navigate(SCREENS.SETTINGS);
|
||||
}, [navigate, SCREENS.SETTINGS]);
|
||||
|
||||
//Обработчик показа информации о приложении
|
||||
const handleShowAbout = React.useCallback(() => {
|
||||
const appInfo = getAppInfo({
|
||||
mode,
|
||||
serverUrl,
|
||||
isDbReady
|
||||
});
|
||||
|
||||
showInfo(appInfo, {
|
||||
title: 'Информация о приложении'
|
||||
});
|
||||
}, [showInfo, mode, serverUrl, isDbReady]);
|
||||
|
||||
//Пункты бокового меню
|
||||
const menuItems = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'Настройки',
|
||||
onPress: handleOpenSettings
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
title: 'О приложении',
|
||||
onPress: handleShowAbout
|
||||
}
|
||||
];
|
||||
}, [handleOpenSettings, handleShowAbout]);
|
||||
|
||||
//Поле сервера показываем, если настройка выключена или сервер не указан
|
||||
const shouldShowServerUrl = isServerUrlFieldVisible(hideServerUrl, serverUrl);
|
||||
|
||||
return (
|
||||
<AdaptiveView padding={false}>
|
||||
<AppHeader showBackButton={canGoBack} onBackPress={handleBackPress} onMenuPress={handleMenuOpen} />
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoidingView}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||
>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.logoContainer}>
|
||||
<AppLogo size="large" />
|
||||
</View>
|
||||
|
||||
<View style={styles.formContainer}>
|
||||
<AppText style={styles.title} variant="h2" weight="bold">
|
||||
Вход в приложение
|
||||
</AppText>
|
||||
|
||||
{shouldShowServerUrl ? (
|
||||
<AppInput
|
||||
ref={serverInputRef}
|
||||
key="server-input"
|
||||
label="Сервер"
|
||||
value={serverUrl}
|
||||
onChangeText={setServerUrl}
|
||||
placeholder="https://example.com/api"
|
||||
keyboardType="url"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disabled={isLoading}
|
||||
blurOnSubmit={false}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={handleServerSubmitEditing}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<AppInput
|
||||
ref={loginInputRef}
|
||||
key="login-input"
|
||||
label="Логин"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
placeholder="Введите логин"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disabled={isLoading || isFromMenu}
|
||||
blurOnSubmit={false}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={handleLoginSubmitEditing}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
ref={passwordInputRef}
|
||||
key="password-input"
|
||||
label="Пароль"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="Введите пароль"
|
||||
showPassword={showPassword}
|
||||
onTogglePassword={handleTogglePassword}
|
||||
disabled={isLoading}
|
||||
blurOnSubmit={true}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handlePasswordSubmitEditing}
|
||||
/>
|
||||
|
||||
<View style={styles.switchContainer}>
|
||||
<AppSwitch label="Сохранить пароль" value={savePassword} onValueChange={handleToggleSavePassword} disabled={isLoading} />
|
||||
</View>
|
||||
|
||||
<AppButton
|
||||
title={isLoading ? 'Вход...' : 'Войти'}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading || !isSettingsLoaded}
|
||||
style={styles.loginButton}
|
||||
textStyle={styles.loginButtonText}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
<SideMenu
|
||||
visible={menuVisible}
|
||||
onClose={handleMenuClose}
|
||||
items={menuItems}
|
||||
title="Меню"
|
||||
mode={mode}
|
||||
username={session?.userName}
|
||||
organization={session?.companyName}
|
||||
/>
|
||||
|
||||
<OrganizationSelectDialog
|
||||
visible={showOrgDialog}
|
||||
organizations={organizations}
|
||||
onSelect={handleSelectOrganization}
|
||||
onCancel={handleCancelOrganization}
|
||||
title="Выберите организацию"
|
||||
/>
|
||||
|
||||
<LoadingOverlay visible={isLoading} message="Выполняется вход..." />
|
||||
</AdaptiveView>
|
||||
);
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = AuthScreen;
|
||||
@ -9,16 +9,22 @@
|
||||
|
||||
const React = require('react'); //React и хуки
|
||||
const { View } = require('react-native'); //Базовые компоненты
|
||||
const { LOAD_STATUS_LOADING } = require('../hooks/usePreTripInspections'); //Константы загрузки
|
||||
const { LOAD_STATUS_LOADING } = require('../config/loadStatus'); //Статусы загрузки
|
||||
const { useAppMessagingContext } = require('../components/layout/AppMessagingProvider'); //Контекст сообщений
|
||||
const { useAppModeContext } = require('../components/layout/AppModeProvider'); //Контекст режима работы
|
||||
const { useAppNavigationContext } = require('../components/layout/AppNavigationProvider'); //Контекст навигации
|
||||
const { useAppLocalDbContext } = require('../components/layout/AppLocalDbProvider'); //Контекст локальной БД
|
||||
const { useAppPreTripInspectionsContext } = require('../components/layout/AppPreTripInspectionsProvider'); //Контекст осмотров
|
||||
const { useAppAuthContext } = require('../components/layout/AppAuthProvider'); //Контекст авторизации
|
||||
const AppHeader = require('../components/layout/AppHeader'); //Заголовок с меню
|
||||
const SideMenu = require('../components/menu/SideMenu'); //Боковое меню
|
||||
const InspectionList = require('../components/inspections/InspectionList'); //Список осмотров
|
||||
const LoadingOverlay = require('../components/common/LoadingOverlay'); //Оверлей загрузки
|
||||
const OrganizationSelectDialog = require('../components/auth/OrganizationSelectDialog'); //Диалог выбора организации
|
||||
const { getAppInfo } = require('../utils/appInfo'); //Информация о приложении
|
||||
const { CONNECTION_LOST_MESSAGE, OFFLINE_MODE_TITLE } = require('../config/messages'); //Сообщения
|
||||
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons'); //Кнопки диалогов
|
||||
const { APP_COLORS } = require('../config/theme'); //Цветовая схема
|
||||
const styles = require('../styles/screens/MainScreen.styles'); //Стили экрана
|
||||
|
||||
//-----------
|
||||
@ -28,14 +34,31 @@ 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 { showInfo, showError, showSuccess } = useAppMessagingContext();
|
||||
const { mode, setOnline, setOffline, setNotConnected } = useAppModeContext();
|
||||
const { navigate, SCREENS, setInitialScreen } = useAppNavigationContext();
|
||||
const { getSetting, isDbReady: isLocalDbReady } = useAppLocalDbContext();
|
||||
const {
|
||||
session,
|
||||
isAuthenticated,
|
||||
isInitialized,
|
||||
logout,
|
||||
checkSession,
|
||||
selectCompany,
|
||||
isLoading: isAuthLoading,
|
||||
sessionChecked,
|
||||
markSessionChecked,
|
||||
getAndClearSessionRestoredFromStorage
|
||||
} = useAppAuthContext();
|
||||
|
||||
const [menuVisible, setMenuVisible] = React.useState(false);
|
||||
const [serverUrl, setServerUrl] = React.useState('');
|
||||
|
||||
//Состояние для диалога выбора организации при проверке сессии
|
||||
const [showOrgDialog, setShowOrgDialog] = React.useState(false);
|
||||
const [organizations, setOrganizations] = React.useState([]);
|
||||
const [pendingSessionData, setPendingSessionData] = React.useState(null);
|
||||
|
||||
//Предотвращение повторной загрузки при монтировании
|
||||
const initialLoadRef = React.useRef(false);
|
||||
|
||||
@ -59,6 +82,65 @@ function MainScreen() {
|
||||
loadServerUrl();
|
||||
}, [isLocalDbReady, getSetting]);
|
||||
|
||||
//Проверка соединения только при открытии приложения и только если пользователь был авторизован и не выходил
|
||||
React.useEffect(() => {
|
||||
if (!isInitialized || sessionChecked) {
|
||||
return;
|
||||
}
|
||||
if (!getAndClearSessionRestoredFromStorage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
markSessionChecked();
|
||||
|
||||
const verifySession = async () => {
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await checkSession();
|
||||
|
||||
if (!result.success) {
|
||||
//Сессия недействительна
|
||||
showInfo('Сессия истекла. Выполните повторный вход.');
|
||||
return;
|
||||
}
|
||||
|
||||
//Проверяем необходимость выбора организации
|
||||
if (result.needSelectCompany) {
|
||||
setOrganizations(result.organizations);
|
||||
setPendingSessionData({
|
||||
serverUrl: result.serverUrl,
|
||||
sessionId: result.sessionId,
|
||||
user: result.user,
|
||||
savePassword: result.savePassword
|
||||
});
|
||||
setShowOrgDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
//Устанавливаем режим работы на основе проверки сервера
|
||||
if (result.isOffline) {
|
||||
setOffline();
|
||||
showInfo(CONNECTION_LOST_MESSAGE, { title: OFFLINE_MODE_TITLE });
|
||||
} else {
|
||||
setOnline();
|
||||
}
|
||||
};
|
||||
|
||||
verifySession();
|
||||
}, [
|
||||
isInitialized,
|
||||
isAuthenticated,
|
||||
sessionChecked,
|
||||
markSessionChecked,
|
||||
getAndClearSessionRestoredFromStorage,
|
||||
checkSession,
|
||||
setOnline,
|
||||
setOffline,
|
||||
showInfo
|
||||
]);
|
||||
|
||||
//Первичная загрузка данных
|
||||
React.useEffect(() => {
|
||||
//Выходим, если БД не готова или уже загружали
|
||||
@ -69,6 +151,44 @@ function MainScreen() {
|
||||
refreshInspections();
|
||||
}, [isDbReady, refreshInspections]);
|
||||
|
||||
//Обработчик выбора организации при проверке сессии
|
||||
const handleSelectOrganization = React.useCallback(
|
||||
async org => {
|
||||
setShowOrgDialog(false);
|
||||
|
||||
if (!pendingSessionData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await selectCompany({
|
||||
serverUrl: pendingSessionData.serverUrl,
|
||||
sessionId: pendingSessionData.sessionId,
|
||||
user: pendingSessionData.user,
|
||||
company: org,
|
||||
savePassword: pendingSessionData.savePassword
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
showError(result.error || 'Ошибка выбора организации');
|
||||
return;
|
||||
}
|
||||
|
||||
setOnline();
|
||||
setPendingSessionData(null);
|
||||
setOrganizations([]);
|
||||
},
|
||||
[pendingSessionData, selectCompany, showError, setOnline]
|
||||
);
|
||||
|
||||
//Обработчик отмены выбора организации
|
||||
const handleCancelOrganization = React.useCallback(() => {
|
||||
setShowOrgDialog(false);
|
||||
setPendingSessionData(null);
|
||||
setOrganizations([]);
|
||||
//При отмене остаемся в оффлайн режиме
|
||||
setOffline();
|
||||
}, [setOffline]);
|
||||
|
||||
//Обработчик открытия меню
|
||||
const handleMenuOpen = React.useCallback(() => {
|
||||
setMenuVisible(true);
|
||||
@ -92,24 +212,81 @@ function MainScreen() {
|
||||
});
|
||||
}, [showInfo, mode, serverUrl, isLocalDbReady]);
|
||||
|
||||
//Обработчик перехода на экран входа
|
||||
const handleLogin = React.useCallback(() => {
|
||||
navigate(SCREENS.AUTH, { fromMenu: true, fromMenuKey: Date.now() });
|
||||
}, [navigate, SCREENS.AUTH]);
|
||||
|
||||
//Обработчик перехода в настройки
|
||||
const handleOpenSettings = React.useCallback(() => {
|
||||
navigate(SCREENS.SETTINGS);
|
||||
}, [navigate, SCREENS.SETTINGS]);
|
||||
|
||||
//Обработчик подтверждения выхода (для диалога)
|
||||
const performLogout = React.useCallback(async () => {
|
||||
const result = await logout({ skipServerRequest: mode === 'OFFLINE' });
|
||||
|
||||
if (result.success) {
|
||||
showSuccess('Выход выполнен');
|
||||
setNotConnected();
|
||||
setInitialScreen(SCREENS.AUTH);
|
||||
} else {
|
||||
showError(result.error || 'Ошибка выхода');
|
||||
}
|
||||
}, [logout, mode, showSuccess, showError, setNotConnected, setInitialScreen, SCREENS.AUTH]);
|
||||
|
||||
//Обработчик выхода из приложения
|
||||
const handleLogout = React.useCallback(() => {
|
||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Выйти', performLogout);
|
||||
|
||||
showInfo('Вы уверены, что хотите выйти?', {
|
||||
title: 'Подтверждение выхода',
|
||||
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
|
||||
});
|
||||
}, [showInfo, performLogout]);
|
||||
|
||||
//Пункты бокового меню
|
||||
const menuItems = React.useMemo(
|
||||
() => [
|
||||
const menuItems = React.useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'Настройки',
|
||||
onPress: () => {
|
||||
navigate(SCREENS.SETTINGS);
|
||||
}
|
||||
onPress: handleOpenSettings
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
title: 'О приложении',
|
||||
onPress: handleShowAbout
|
||||
}
|
||||
],
|
||||
[navigate, handleShowAbout, SCREENS.SETTINGS]
|
||||
);
|
||||
];
|
||||
|
||||
//Добавляем разделитель перед кнопками авторизации
|
||||
items.push({
|
||||
id: 'divider',
|
||||
type: 'divider'
|
||||
});
|
||||
|
||||
//Кнопка "Вход" для оффлайн режима
|
||||
if (mode === 'OFFLINE') {
|
||||
items.push({
|
||||
id: 'login',
|
||||
title: 'Вход',
|
||||
onPress: handleLogin
|
||||
});
|
||||
}
|
||||
|
||||
//Кнопка "Выход" для онлайн/оффлайн режима (когда есть сессия)
|
||||
if ((mode === 'ONLINE' || mode === 'OFFLINE') && isAuthenticated) {
|
||||
items.push({
|
||||
id: 'logout',
|
||||
title: 'Выход',
|
||||
onPress: handleLogout,
|
||||
textStyle: { color: APP_COLORS.error }
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [handleOpenSettings, handleShowAbout, handleLogin, handleLogout, mode, isAuthenticated]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@ -124,7 +301,25 @@ function MainScreen() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<SideMenu visible={menuVisible} onClose={handleMenuClose} items={menuItems} title="Меню" />
|
||||
<SideMenu
|
||||
visible={menuVisible}
|
||||
onClose={handleMenuClose}
|
||||
items={menuItems}
|
||||
title="Меню"
|
||||
mode={mode}
|
||||
username={session?.userName}
|
||||
organization={session?.companyName}
|
||||
/>
|
||||
|
||||
<OrganizationSelectDialog
|
||||
visible={showOrgDialog}
|
||||
organizations={organizations}
|
||||
onSelect={handleSelectOrganization}
|
||||
onCancel={handleCancelOrganization}
|
||||
title="Выберите организацию"
|
||||
/>
|
||||
|
||||
<LoadingOverlay visible={isAuthLoading} message="Выполняется операция..." />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ 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 AppSwitch = require('../components/common/AppSwitch');
|
||||
const CopyButton = require('../components/common/CopyButton');
|
||||
const InputDialog = require('../components/common/InputDialog');
|
||||
const AppHeader = require('../components/layout/AppHeader');
|
||||
@ -19,8 +20,11 @@ const { useAppMessagingContext } = require('../components/layout/AppMessagingPro
|
||||
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 { useAppAuthContext } = require('../components/layout/AppAuthProvider');
|
||||
const { AUTH_SETTINGS_KEYS, DEFAULT_IDLE_TIMEOUT } = require('../config/authConfig');
|
||||
const { DIALOG_BUTTON_TYPE, DIALOG_CANCEL_BUTTON, getConfirmButtonOptions } = require('../config/dialogButtons');
|
||||
const { getAppInfo, getModeLabel } = require('../utils/appInfo');
|
||||
const { validateServerUrlAllowEmpty, validateIdleTimeout } = require('../utils/validation');
|
||||
const styles = require('../styles/screens/SettingsScreen.styles');
|
||||
|
||||
//-----------
|
||||
@ -29,18 +33,23 @@ const styles = require('../styles/screens/SettingsScreen.styles');
|
||||
|
||||
function SettingsScreen() {
|
||||
const { showInfo, showError, showSuccess } = useAppMessagingContext();
|
||||
const { mode, setOnline, setNotConnected } = useAppModeContext();
|
||||
const { APP_MODE, mode, setNotConnected } = useAppModeContext();
|
||||
const { goBack, canGoBack } = useAppNavigationContext();
|
||||
const { getSetting, setSetting, clearSettings, clearInspections, vacuum, isDbReady } = useAppLocalDbContext();
|
||||
const { session, isAuthenticated, getDeviceId } = useAppAuthContext();
|
||||
|
||||
const [serverUrl, setServerUrl] = React.useState('');
|
||||
const [hideServerUrl, setHideServerUrl] = React.useState(false);
|
||||
const [idleTimeout, setIdleTimeout] = React.useState('');
|
||||
const [deviceId, setDeviceId] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [isServerUrlDialogVisible, setIsServerUrlDialogVisible] = React.useState(false);
|
||||
const [isIdleTimeoutDialogVisible, setIsIdleTimeoutDialogVisible] = React.useState(false);
|
||||
|
||||
//Предотвращение повторной загрузки настроек
|
||||
const settingsLoadedRef = React.useRef(false);
|
||||
|
||||
//Загрузка сохраненного URL сервера при готовности БД
|
||||
//Загрузка сохраненных настроек при готовности БД
|
||||
React.useEffect(() => {
|
||||
//Выходим, если БД не готова или уже загрузили настройки
|
||||
if (!isDbReady || settingsLoadedRef.current) {
|
||||
@ -52,10 +61,27 @@ function SettingsScreen() {
|
||||
const loadSettings = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const savedUrl = await getSetting('app_server_url');
|
||||
const savedUrl = await getSetting(AUTH_SETTINGS_KEYS.SERVER_URL);
|
||||
if (savedUrl) {
|
||||
setServerUrl(savedUrl);
|
||||
}
|
||||
|
||||
const savedHideServerUrl = await getSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL);
|
||||
setHideServerUrl(savedHideServerUrl === 'true' || savedHideServerUrl === true);
|
||||
|
||||
const savedIdleTimeout = await getSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT);
|
||||
if (savedIdleTimeout) {
|
||||
setIdleTimeout(savedIdleTimeout);
|
||||
} else {
|
||||
//Устанавливаем значение по умолчанию
|
||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
||||
setIdleTimeout(defaultValue);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
}
|
||||
|
||||
//Получаем или генерируем идентификатор устройства
|
||||
const currentDeviceId = await getDeviceId();
|
||||
setDeviceId(currentDeviceId || '');
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки настроек:', error);
|
||||
showError('Не удалось загрузить настройки');
|
||||
@ -65,52 +91,56 @@ function SettingsScreen() {
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, [isDbReady, getSetting, showError]);
|
||||
}, [isDbReady, getSetting, setSetting, showError, getDeviceId]);
|
||||
|
||||
//Открытие диалога ввода URL сервера
|
||||
//Доступность редактирования URL сервера (только в режиме "Не подключено")
|
||||
const isServerUrlEditable = React.useMemo(
|
||||
() => !isLoading && isDbReady && mode === APP_MODE.NOT_CONNECTED,
|
||||
[isLoading, isDbReady, mode, APP_MODE.NOT_CONNECTED]
|
||||
);
|
||||
|
||||
//Стиль поля URL сервера при нажатии
|
||||
const getServerUrlFieldPressableStyle = React.useCallback(
|
||||
({ pressed }) => [
|
||||
styles.serverUrlField,
|
||||
isServerUrlEditable ? (pressed ? styles.serverUrlFieldPressed : null) : styles.serverUrlFieldDisabled
|
||||
],
|
||||
[isServerUrlEditable]
|
||||
);
|
||||
|
||||
//Стиль поля времени простоя при нажатии
|
||||
const getIdleTimeoutFieldPressableStyle = React.useCallback(
|
||||
({ pressed }) => [styles.serverUrlField, pressed && styles.serverUrlFieldPressed],
|
||||
[]
|
||||
);
|
||||
|
||||
//Открытие диалога ввода URL сервера (только в режиме "Не подключено")
|
||||
const handleOpenServerUrlDialog = React.useCallback(() => {
|
||||
if (!isServerUrlEditable) {
|
||||
return;
|
||||
}
|
||||
setIsServerUrlDialogVisible(true);
|
||||
}, []);
|
||||
}, [isServerUrlEditable]);
|
||||
|
||||
//Закрытие диалога ввода 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);
|
||||
|
||||
const valueToSave = url != null ? String(url).trim() : '';
|
||||
|
||||
try {
|
||||
const success = await setSetting('app_server_url', url);
|
||||
const success = await setSetting(AUTH_SETTINGS_KEYS.SERVER_URL, valueToSave);
|
||||
|
||||
if (success) {
|
||||
setServerUrl(url);
|
||||
setServerUrl(valueToSave);
|
||||
showSuccess('Настройки сервера сохранены');
|
||||
|
||||
if (mode === 'NOT_CONNECTED') {
|
||||
setOnline();
|
||||
}
|
||||
} else {
|
||||
showError('Не удалось сохранить настройки');
|
||||
}
|
||||
@ -121,76 +151,140 @@ function SettingsScreen() {
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[mode, setOnline, setSetting, showError, showSuccess]
|
||||
[setSetting, showError, showSuccess]
|
||||
);
|
||||
|
||||
//Переключатель скрытия URL сервера в окне логина
|
||||
const handleToggleHideServerUrl = React.useCallback(
|
||||
async value => {
|
||||
try {
|
||||
const success = await setSetting(AUTH_SETTINGS_KEYS.HIDE_SERVER_URL, value ? 'true' : 'false');
|
||||
|
||||
if (success) {
|
||||
setHideServerUrl(value);
|
||||
showSuccess('Настройка сохранена');
|
||||
} else {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения настройки:', error);
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
},
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Открытие диалога ввода времени простоя
|
||||
const handleOpenIdleTimeoutDialog = React.useCallback(() => {
|
||||
setIsIdleTimeoutDialogVisible(true);
|
||||
}, []);
|
||||
|
||||
//Закрытие диалога ввода времени простоя
|
||||
const handleCloseIdleTimeoutDialog = React.useCallback(() => {
|
||||
setIsIdleTimeoutDialogVisible(false);
|
||||
}, []);
|
||||
|
||||
//Сохранение времени простоя
|
||||
const handleSaveIdleTimeout = React.useCallback(
|
||||
async value => {
|
||||
setIsIdleTimeoutDialogVisible(false);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const trimmedValue = value ? value.trim() : '';
|
||||
const success = await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, trimmedValue);
|
||||
|
||||
if (success) {
|
||||
setIdleTimeout(trimmedValue);
|
||||
showSuccess('Время простоя сохранено');
|
||||
} else {
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения времени простоя:', error);
|
||||
showError('Не удалось сохранить настройку');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setSetting, showSuccess, showError]
|
||||
);
|
||||
|
||||
//Выполнение очистки кэша (для диалога подтверждения)
|
||||
const performClearCache = React.useCallback(async () => {
|
||||
try {
|
||||
const success = await clearInspections();
|
||||
if (success) {
|
||||
showSuccess('Кэш успешно очищен');
|
||||
} else {
|
||||
showError('Не удалось очистить кэш');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки кэша:', error);
|
||||
showError('Не удалось очистить кэш');
|
||||
}
|
||||
}, [showSuccess, showError, clearInspections]);
|
||||
|
||||
//Очистка кэша (осмотров)
|
||||
const handleClearCache = React.useCallback(async () => {
|
||||
const handleClearCache = React.useCallback(() => {
|
||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.ERROR, 'Очистить', performClearCache);
|
||||
|
||||
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 }
|
||||
}
|
||||
]
|
||||
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
|
||||
});
|
||||
}, [showInfo, showSuccess, showError, clearInspections]);
|
||||
}, [showInfo, performClearCache]);
|
||||
|
||||
//Выполнение сброса настроек (для диалога подтверждения)
|
||||
//Подключён (онлайн/офлайн): сбрасываем только непричастные к подключению настройки; не подключён: полный сброс
|
||||
const performResetSettings = React.useCallback(async () => {
|
||||
try {
|
||||
const defaultValue = String(DEFAULT_IDLE_TIMEOUT);
|
||||
|
||||
if (mode === APP_MODE.NOT_CONNECTED) {
|
||||
const success = await clearSettings();
|
||||
if (success) {
|
||||
setServerUrl('');
|
||||
setHideServerUrl(false);
|
||||
setIdleTimeout(defaultValue);
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
setNotConnected();
|
||||
showSuccess('Настройки сброшены');
|
||||
} else {
|
||||
showError('Не удалось сбросить настройки');
|
||||
}
|
||||
} else {
|
||||
//Подключён (онлайн или офлайн): сбрасываем только время простоя
|
||||
await setSetting(AUTH_SETTINGS_KEYS.IDLE_TIMEOUT, defaultValue);
|
||||
setIdleTimeout(defaultValue);
|
||||
showSuccess('Настройки сброшены');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка сброса настроек:', error);
|
||||
showError('Не удалось сбросить настройки');
|
||||
}
|
||||
}, [
|
||||
mode,
|
||||
APP_MODE.NOT_CONNECTED,
|
||||
setServerUrl,
|
||||
setHideServerUrl,
|
||||
setIdleTimeout,
|
||||
setNotConnected,
|
||||
clearSettings,
|
||||
setSetting,
|
||||
showSuccess,
|
||||
showError
|
||||
]);
|
||||
|
||||
//Сброс настроек: при подключении (онлайн/офлайн) — без настроек подключения; при отсутствии подключения — все настройки
|
||||
const handleResetSettings = React.useCallback(() => {
|
||||
const confirmButton = getConfirmButtonOptions(DIALOG_BUTTON_TYPE.WARNING, 'Сбросить', performResetSettings);
|
||||
|
||||
//Сброс всех настроек
|
||||
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 }
|
||||
}
|
||||
]
|
||||
buttons: [DIALOG_CANCEL_BUTTON, confirmButton]
|
||||
});
|
||||
}, [showInfo, showSuccess, showError, setNotConnected, clearSettings]);
|
||||
}, [showInfo, performResetSettings]);
|
||||
|
||||
//Оптимизация базы данных
|
||||
const handleOptimizeDb = React.useCallback(async () => {
|
||||
@ -240,11 +334,42 @@ function SettingsScreen() {
|
||||
showError('Не удалось скопировать адрес');
|
||||
}, [showError]);
|
||||
|
||||
//Обработчик копирования идентификатора устройства
|
||||
const handleCopyDeviceId = React.useCallback(() => {
|
||||
showSuccess('Идентификатор устройства скопирован');
|
||||
}, [showSuccess]);
|
||||
|
||||
return (
|
||||
<AdaptiveView padding={false}>
|
||||
<AppHeader showBackButton={true} onBackPress={handleBackPress} showMenuButton={false} />
|
||||
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
{isAuthenticated && session ? (
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Информация о подключении
|
||||
</AppText>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<AppText style={styles.infoLabel} variant="body">
|
||||
Имя пользователя:
|
||||
</AppText>
|
||||
<AppText style={styles.infoValue} variant="body" weight="medium">
|
||||
{session.userName || session.userCode || 'Не указано'}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
<View style={styles.infoRow}>
|
||||
<AppText style={styles.infoLabel} variant="body">
|
||||
Организация:
|
||||
</AppText>
|
||||
<AppText style={styles.infoValue} variant="body" weight="medium">
|
||||
{session.companyName || 'Не указана'}
|
||||
</AppText>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Сервер приложений
|
||||
@ -255,13 +380,13 @@ function SettingsScreen() {
|
||||
</AppText>
|
||||
|
||||
<View style={styles.serverUrlRow}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.serverUrlField, pressed && styles.serverUrlFieldPressed]}
|
||||
onPress={handleOpenServerUrlDialog}
|
||||
disabled={isLoading || !isDbReady}
|
||||
>
|
||||
<Pressable style={getServerUrlFieldPressableStyle} onPress={handleOpenServerUrlDialog} disabled={!isServerUrlEditable}>
|
||||
<AppText
|
||||
style={[styles.serverUrlText, !serverUrl && styles.serverUrlPlaceholder]}
|
||||
style={[
|
||||
styles.serverUrlText,
|
||||
!serverUrl && styles.serverUrlPlaceholder,
|
||||
!isServerUrlEditable && styles.serverUrlTextDisabled
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{serverUrl || 'Нажмите для ввода адреса'}
|
||||
@ -269,12 +394,48 @@ function SettingsScreen() {
|
||||
</Pressable>
|
||||
|
||||
{serverUrl ? (
|
||||
<CopyButton
|
||||
value={serverUrl}
|
||||
onCopy={handleCopyServerUrl}
|
||||
onError={handleCopyError}
|
||||
style={styles.serverUrlCopyButton}
|
||||
/>
|
||||
<CopyButton value={serverUrl} onCopy={handleCopyServerUrl} onError={handleCopyError} style={styles.serverUrlCopyButton} />
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.switchRow}>
|
||||
<AppSwitch
|
||||
label="Не отображать адрес сервера в окне логина"
|
||||
value={hideServerUrl}
|
||||
onValueChange={handleToggleHideServerUrl}
|
||||
disabled={isLoading || !isDbReady}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<AppText style={styles.sectionTitle} variant="h3" weight="semibold">
|
||||
Системные настройки
|
||||
</AppText>
|
||||
|
||||
<AppText style={styles.fieldLabel} variant="caption" weight="medium">
|
||||
Максимальное время простоя (минут)
|
||||
</AppText>
|
||||
|
||||
<Pressable style={getIdleTimeoutFieldPressableStyle} onPress={handleOpenIdleTimeoutDialog} disabled={isLoading || !isDbReady}>
|
||||
<AppText style={styles.serverUrlText} numberOfLines={1}>
|
||||
{idleTimeout || String(DEFAULT_IDLE_TIMEOUT)}
|
||||
</AppText>
|
||||
</Pressable>
|
||||
|
||||
<AppText style={[styles.fieldLabel, styles.fieldLabelMarginTop]} variant="caption" weight="medium">
|
||||
Идентификатор устройства
|
||||
</AppText>
|
||||
|
||||
<View style={styles.serverUrlRow}>
|
||||
<View style={[styles.serverUrlField, styles.deviceIdField]}>
|
||||
<AppText style={styles.serverUrlText} numberOfLines={1}>
|
||||
{deviceId || 'Загрузка...'}
|
||||
</AppText>
|
||||
</View>
|
||||
|
||||
{deviceId ? (
|
||||
<CopyButton value={deviceId} onCopy={handleCopyDeviceId} onError={handleCopyError} style={styles.serverUrlCopyButton} />
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
@ -334,12 +495,7 @@ function SettingsScreen() {
|
||||
{serverUrl || 'Не настроен'}
|
||||
</AppText>
|
||||
{serverUrl ? (
|
||||
<CopyButton
|
||||
value={serverUrl}
|
||||
onCopy={handleCopyServerUrl}
|
||||
onError={handleCopyError}
|
||||
style={styles.copyButton}
|
||||
/>
|
||||
<CopyButton value={serverUrl} onCopy={handleCopyServerUrl} onError={handleCopyError} style={styles.copyButton} />
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
@ -366,7 +522,21 @@ function SettingsScreen() {
|
||||
cancelText="Отмена"
|
||||
onConfirm={handleSaveServerUrl}
|
||||
onCancel={handleCloseServerUrlDialog}
|
||||
validator={validateServerUrl}
|
||||
validator={validateServerUrlAllowEmpty}
|
||||
/>
|
||||
|
||||
<InputDialog
|
||||
visible={isIdleTimeoutDialogVisible}
|
||||
title="Время простоя"
|
||||
label="Максимальное время простоя (минут)"
|
||||
value={idleTimeout}
|
||||
placeholder="Например: 30"
|
||||
keyboardType="numeric"
|
||||
confirmText="Сохранить"
|
||||
cancelText="Отмена"
|
||||
onConfirm={handleSaveIdleTimeout}
|
||||
onCancel={handleCloseIdleTimeoutDialog}
|
||||
validator={validateIdleTimeout}
|
||||
/>
|
||||
</AdaptiveView>
|
||||
);
|
||||
|
||||
142
rn/app/src/styles/auth/OrganizationSelectDialog.styles.js
Normal file
142
rn/app/src/styles/auth/OrganizationSelectDialog.styles.js
Normal file
@ -0,0 +1,142 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили диалога выбора организации
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { APP_COLORS } = require('../../config/theme');
|
||||
const { UI } = require('../../config/appConfig');
|
||||
const { responsiveSpacing, heightPercentage } = require('../../utils/responsive');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: APP_COLORS.overlay,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: UI.PADDING
|
||||
},
|
||||
dialog: {
|
||||
backgroundColor: APP_COLORS.surface,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
maxHeight: heightPercentage(80),
|
||||
flexDirection: 'column'
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: UI.PADDING,
|
||||
paddingTop: UI.PADDING,
|
||||
paddingBottom: responsiveSpacing(3),
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: APP_COLORS.borderSubtle,
|
||||
position: 'relative'
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
color: APP_COLORS.textPrimary,
|
||||
flex: 1,
|
||||
paddingHorizontal: 40
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: UI.PADDING,
|
||||
top: UI.PADDING,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
closeButtonPressed: {
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
closeIcon: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
closeIconLine1: {
|
||||
position: 'absolute',
|
||||
width: 16,
|
||||
height: 2,
|
||||
backgroundColor: APP_COLORS.textSecondary,
|
||||
borderRadius: 1,
|
||||
transform: [{ rotate: '45deg' }]
|
||||
},
|
||||
closeIconLine2: {
|
||||
position: 'absolute',
|
||||
width: 16,
|
||||
height: 2,
|
||||
backgroundColor: APP_COLORS.textSecondary,
|
||||
borderRadius: 1,
|
||||
transform: [{ rotate: '-45deg' }]
|
||||
},
|
||||
list: {
|
||||
flexGrow: 0,
|
||||
flexShrink: 1
|
||||
},
|
||||
listContent: {
|
||||
paddingVertical: responsiveSpacing(2)
|
||||
},
|
||||
organizationItem: {
|
||||
paddingHorizontal: UI.PADDING,
|
||||
paddingVertical: responsiveSpacing(3),
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: APP_COLORS.borderSubtle
|
||||
},
|
||||
organizationItemSelected: {
|
||||
backgroundColor: APP_COLORS.primaryExtraLight
|
||||
},
|
||||
organizationItemPressed: {
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
organizationName: {
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
organizationNameSelected: {
|
||||
color: APP_COLORS.primary,
|
||||
fontWeight: '600'
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
padding: UI.PADDING,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: APP_COLORS.borderSubtle,
|
||||
gap: responsiveSpacing(3),
|
||||
flexShrink: 0
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
confirmButton: {
|
||||
flex: 1,
|
||||
backgroundColor: APP_COLORS.primary
|
||||
},
|
||||
confirmButtonText: {
|
||||
color: APP_COLORS.white
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -9,122 +9,51 @@
|
||||
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { responsiveSize } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
const { LOGO_SIZE_KEYS } = require('../../config/appAssets'); //Ключи размеров логотипа
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Цвета иконки
|
||||
const TEAL_BACKGROUND = '#50AF95';
|
||||
const ROBOT_WHITE = '#FFFFFF';
|
||||
//Размеры контейнера для вариантов логотипа
|
||||
const SIZES = Object.freeze({
|
||||
[LOGO_SIZE_KEYS.SMALL]: responsiveSize(32),
|
||||
[LOGO_SIZE_KEYS.MEDIUM]: responsiveSize(40),
|
||||
[LOGO_SIZE_KEYS.LARGE]: responsiveSize(56)
|
||||
});
|
||||
|
||||
//Размеры для разных вариантов
|
||||
const SIZES = {
|
||||
small: responsiveSize(32),
|
||||
medium: responsiveSize(40),
|
||||
large: responsiveSize(56)
|
||||
};
|
||||
//Радиус скругления контейнера по размерам
|
||||
const BORDER_RADIUS = Object.freeze({
|
||||
[LOGO_SIZE_KEYS.SMALL]: responsiveSize(6),
|
||||
[LOGO_SIZE_KEYS.MEDIUM]: responsiveSize(8),
|
||||
[LOGO_SIZE_KEYS.LARGE]: responsiveSize(10)
|
||||
});
|
||||
|
||||
//Стили логотипа
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: TEAL_BACKGROUND,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
containerSmall: {
|
||||
width: SIZES.small,
|
||||
height: SIZES.small,
|
||||
borderRadius: responsiveSize(6)
|
||||
width: SIZES[LOGO_SIZE_KEYS.SMALL],
|
||||
height: SIZES[LOGO_SIZE_KEYS.SMALL],
|
||||
borderRadius: BORDER_RADIUS[LOGO_SIZE_KEYS.SMALL]
|
||||
},
|
||||
containerMedium: {
|
||||
width: SIZES.medium,
|
||||
height: SIZES.medium,
|
||||
borderRadius: responsiveSize(8)
|
||||
width: SIZES[LOGO_SIZE_KEYS.MEDIUM],
|
||||
height: SIZES[LOGO_SIZE_KEYS.MEDIUM],
|
||||
borderRadius: BORDER_RADIUS[LOGO_SIZE_KEYS.MEDIUM]
|
||||
},
|
||||
containerLarge: {
|
||||
width: SIZES.large,
|
||||
height: SIZES.large,
|
||||
borderRadius: responsiveSize(10)
|
||||
width: SIZES[LOGO_SIZE_KEYS.LARGE],
|
||||
height: SIZES[LOGO_SIZE_KEYS.LARGE],
|
||||
borderRadius: BORDER_RADIUS[LOGO_SIZE_KEYS.LARGE]
|
||||
},
|
||||
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)
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require("react-native"); //StyleSheet React Native
|
||||
const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -19,12 +19,12 @@ const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
flex: 1,
|
||||
backgroundColor: APP_COLORS.overlay,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24
|
||||
},
|
||||
container: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
@ -55,15 +55,15 @@ const styles = StyleSheet.create({
|
||||
borderLeftColor: APP_COLORS.success
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
fontWeight: '600',
|
||||
color: APP_COLORS.textPrimary,
|
||||
marginRight: 8
|
||||
},
|
||||
@ -71,8 +71,8 @@ const styles = StyleSheet.create({
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
closeButtonText: {
|
||||
fontSize: 20,
|
||||
@ -87,8 +87,8 @@ const styles = StyleSheet.create({
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
buttonsRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8
|
||||
},
|
||||
buttonBase: {
|
||||
@ -102,7 +102,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
fontWeight: '500',
|
||||
color: APP_COLORS.white
|
||||
}
|
||||
});
|
||||
@ -112,4 +112,3 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
|
||||
42
rn/app/src/styles/common/AppSwitch.styles.js
Normal file
42
rn/app/src/styles/common/AppSwitch.styles.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили компонента переключателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { APP_COLORS } = require('../../config/theme');
|
||||
const { responsiveSpacing } = require('../../utils/responsive');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: responsiveSpacing(2)
|
||||
},
|
||||
containerDisabled: {
|
||||
opacity: 0.5
|
||||
},
|
||||
label: {
|
||||
flex: 1,
|
||||
marginRight: responsiveSpacing(3),
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
labelDisabled: {
|
||||
color: APP_COLORS.textTertiary
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
55
rn/app/src/styles/common/LoadingOverlay.styles.js
Normal file
55
rn/app/src/styles/common/LoadingOverlay.styles.js
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили компонента оверлея загрузки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
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({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: APP_COLORS.overlay,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
container: {
|
||||
backgroundColor: APP_COLORS.surface,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
padding: responsiveSpacing(6),
|
||||
alignItems: 'center',
|
||||
minWidth: 150,
|
||||
shadowColor: APP_COLORS.shadow,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5
|
||||
},
|
||||
indicator: {
|
||||
color: APP_COLORS.primary
|
||||
},
|
||||
message: {
|
||||
marginTop: responsiveSpacing(4),
|
||||
color: APP_COLORS.textPrimary,
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
92
rn/app/src/styles/common/PasswordInput.styles.js
Normal file
92
rn/app/src/styles/common/PasswordInput.styles.js
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили компонента ввода пароля
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
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({
|
||||
container: {
|
||||
marginBottom: responsiveSpacing(4)
|
||||
},
|
||||
label: {
|
||||
marginBottom: responsiveSpacing(2),
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: APP_COLORS.borderMedium,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
backgroundColor: APP_COLORS.white,
|
||||
minHeight: UI.INPUT_HEIGHT
|
||||
},
|
||||
inputContainerFocused: {
|
||||
borderColor: APP_COLORS.primary,
|
||||
borderWidth: 2,
|
||||
shadowColor: APP_COLORS.primary,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 2
|
||||
},
|
||||
inputContainerError: {
|
||||
borderColor: APP_COLORS.error
|
||||
},
|
||||
inputContainerDisabled: {
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
paddingHorizontal: responsiveSpacing(3),
|
||||
paddingVertical: responsiveSpacing(2.5),
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary,
|
||||
includeFontPadding: false,
|
||||
textAlignVertical: 'center'
|
||||
},
|
||||
inputDisabled: {
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
placeholder: {
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
toggleButton: {
|
||||
paddingHorizontal: responsiveSpacing(3),
|
||||
paddingVertical: responsiveSpacing(2),
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
toggleButtonText: {
|
||||
color: APP_COLORS.primary,
|
||||
fontSize: UI.FONT_SIZE_SM
|
||||
},
|
||||
toggleButtonTextDisabled: {
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
helperText: {
|
||||
marginTop: responsiveSpacing(1),
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
helperTextError: {
|
||||
color: APP_COLORS.error
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -7,8 +7,8 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require("react-native"); //StyleSheet React Native
|
||||
const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -25,12 +25,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
fontWeight: '500',
|
||||
marginBottom: 4
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
meta: {
|
||||
fontSize: 12,
|
||||
@ -43,4 +43,3 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require("react-native"); //StyleSheet React Native
|
||||
const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -18,23 +18,26 @@ const { APP_COLORS } = require("../../config/theme"); //Цветовая схе
|
||||
const styles = StyleSheet.create({
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 32
|
||||
},
|
||||
centerText: {
|
||||
marginTop: 12,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
errorText: {
|
||||
marginTop: 8,
|
||||
textAlign: "center",
|
||||
textAlign: 'center',
|
||||
color: APP_COLORS.error,
|
||||
fontSize: 12
|
||||
},
|
||||
centerButton: {
|
||||
marginTop: 16
|
||||
},
|
||||
indicator: {
|
||||
color: APP_COLORS.primary
|
||||
}
|
||||
});
|
||||
|
||||
@ -43,4 +46,3 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
|
||||
17
rn/app/src/styles/layout/AppAuthProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppAuthProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера авторизации (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -7,8 +7,8 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require("react-native"); //StyleSheet React Native
|
||||
const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -18,13 +18,13 @@ const { APP_COLORS } = require("../../config/theme"); //Цветовая схе
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: APP_COLORS.background,
|
||||
padding: 24
|
||||
},
|
||||
card: {
|
||||
width: "100%",
|
||||
width: '100%',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
@ -40,7 +40,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
fontWeight: '600',
|
||||
color: APP_COLORS.error,
|
||||
marginBottom: 8
|
||||
},
|
||||
@ -50,8 +50,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 16
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end"
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end'
|
||||
}
|
||||
});
|
||||
|
||||
@ -60,4 +60,3 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
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 { responsiveSize, responsiveSpacing } = require('../../utils/responsive'); //Адаптивные утилиты
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -91,59 +91,6 @@ const styles = StyleSheet.create({
|
||||
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),
|
||||
|
||||
17
rn/app/src/styles/layout/AppLocalDbProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppLocalDbProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера локальной БД (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
17
rn/app/src/styles/layout/AppMessagingProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppMessagingProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера сообщений (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
17
rn/app/src/styles/layout/AppModeProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppModeProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера режима работы (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
17
rn/app/src/styles/layout/AppNavigationProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppNavigationProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера навигации (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера предрейсовых осмотров (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
17
rn/app/src/styles/layout/AppServerProvider.styles.js
Normal file
17
rn/app/src/styles/layout/AppServerProvider.styles.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили провайдера сервера приложений (провайдер без визуальной разметки)
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Провайдер не рендерит собственных View — стили не требуются
|
||||
const styles = {};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -7,8 +7,8 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require("react-native"); //StyleSheet React Native
|
||||
const { APP_COLORS } = require("../../config/theme"); //Цветовая схема приложения
|
||||
const { StyleSheet } = require('react-native'); //StyleSheet React Native
|
||||
const { APP_COLORS } = require('../../config/theme'); //Цветовая схема приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -27,4 +27,3 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
|
||||
82
rn/app/src/styles/menu/MenuUserInfo.styles.js
Normal file
82
rn/app/src/styles/menu/MenuUserInfo.styles.js
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили компонента информации о пользователе в меню
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { APP_COLORS } = require('../../config/theme');
|
||||
const { UI } = require('../../config/appConfig');
|
||||
const { responsiveSpacing, responsiveSize } = require('../../utils/responsive');
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: UI.PADDING,
|
||||
paddingVertical: responsiveSpacing(3),
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: APP_COLORS.borderSubtle,
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
modeContainer: {
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: responsiveSpacing(3),
|
||||
paddingVertical: responsiveSpacing(1.5),
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
marginBottom: responsiveSpacing(2)
|
||||
},
|
||||
modeOnline: {
|
||||
backgroundColor: APP_COLORS.success + '20'
|
||||
},
|
||||
modeOffline: {
|
||||
backgroundColor: APP_COLORS.warning + '20'
|
||||
},
|
||||
modeNotConnected: {
|
||||
backgroundColor: APP_COLORS.textSecondary + '20'
|
||||
},
|
||||
modeText: {
|
||||
fontSize: responsiveSize(13),
|
||||
fontWeight: '600'
|
||||
},
|
||||
modeTextOnline: {
|
||||
color: APP_COLORS.success
|
||||
},
|
||||
modeTextOffline: {
|
||||
color: APP_COLORS.warning
|
||||
},
|
||||
modeTextNotConnected: {
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
userInfo: {
|
||||
marginTop: responsiveSpacing(1)
|
||||
},
|
||||
userRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: responsiveSpacing(1)
|
||||
},
|
||||
userLabel: {
|
||||
fontSize: responsiveSize(12),
|
||||
color: APP_COLORS.textSecondary,
|
||||
marginRight: responsiveSpacing(1),
|
||||
flexShrink: 0
|
||||
},
|
||||
userValue: {
|
||||
fontSize: responsiveSize(13),
|
||||
color: APP_COLORS.textPrimary,
|
||||
fontWeight: '500',
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
70
rn/app/src/styles/screens/AuthScreen.styles.js
Normal file
70
rn/app/src/styles/screens/AuthScreen.styles.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Стили экрана аутентификации
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
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({
|
||||
keyboardAvoidingView: {
|
||||
flex: 1
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: UI.PADDING,
|
||||
paddingBottom: responsiveSpacing(8)
|
||||
},
|
||||
logoContainer: {
|
||||
alignItems: 'center',
|
||||
paddingTop: responsiveSpacing(6),
|
||||
paddingBottom: responsiveSpacing(4)
|
||||
},
|
||||
formContainer: {
|
||||
backgroundColor: APP_COLORS.surface,
|
||||
borderRadius: UI.BORDER_RADIUS,
|
||||
padding: UI.PADDING,
|
||||
borderWidth: 1,
|
||||
borderColor: APP_COLORS.borderSubtle
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginBottom: responsiveSpacing(6),
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
switchContainer: {
|
||||
marginBottom: responsiveSpacing(4)
|
||||
},
|
||||
loginButton: {
|
||||
backgroundColor: APP_COLORS.primary,
|
||||
marginTop: responsiveSpacing(2)
|
||||
},
|
||||
loginButtonText: {
|
||||
color: APP_COLORS.white,
|
||||
fontWeight: '600'
|
||||
},
|
||||
hint: {
|
||||
textAlign: 'center',
|
||||
marginTop: responsiveSpacing(4),
|
||||
color: APP_COLORS.textTertiary
|
||||
}
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
@ -6,6 +6,7 @@
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { APP_COLORS } = require('../../config/theme');
|
||||
const { UI } = require('../../config/appConfig');
|
||||
@ -39,6 +40,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: responsiveSpacing(2),
|
||||
color: APP_COLORS.textSecondary
|
||||
},
|
||||
fieldLabelMarginTop: {
|
||||
marginTop: responsiveSpacing(4)
|
||||
},
|
||||
serverUrlRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
@ -57,16 +61,33 @@ const styles = StyleSheet.create({
|
||||
borderColor: APP_COLORS.primary,
|
||||
backgroundColor: APP_COLORS.primaryExtraLight
|
||||
},
|
||||
serverUrlFieldDisabled: {
|
||||
backgroundColor: APP_COLORS.surfaceAlt,
|
||||
borderColor: APP_COLORS.borderSubtle
|
||||
},
|
||||
serverUrlText: {
|
||||
fontSize: UI.FONT_SIZE_MD,
|
||||
color: APP_COLORS.textPrimary
|
||||
},
|
||||
serverUrlTextDisabled: {
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
serverUrlPlaceholder: {
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
serverUrlCopyButton: {
|
||||
marginLeft: responsiveSpacing(2)
|
||||
},
|
||||
deviceIdField: {
|
||||
backgroundColor: APP_COLORS.surfaceAlt
|
||||
},
|
||||
helperText: {
|
||||
marginTop: responsiveSpacing(2),
|
||||
color: APP_COLORS.textTertiary
|
||||
},
|
||||
switchRow: {
|
||||
marginTop: responsiveSpacing(3)
|
||||
},
|
||||
actionButton: {
|
||||
marginTop: responsiveSpacing(3)
|
||||
},
|
||||
@ -124,4 +145,5 @@ const styles = StyleSheet.create({
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = styles;
|
||||
|
||||
227
rn/app/src/utils/deviceId.js
Normal file
227
rn/app/src/utils/deviceId.js
Normal file
@ -0,0 +1,227 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Утилита для получения постоянного идентификатора устройства
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { Platform, NativeModules } = require('react-native');
|
||||
const { WEB_DEVICE_ID_KEY } = require('../config/storageKeys'); //Ключи хранилища
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Генерация хэша из строки
|
||||
const hashString = str => {
|
||||
let hash = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash * 31 + char) % 2147483647;
|
||||
}
|
||||
|
||||
return Math.abs(hash).toString(36);
|
||||
};
|
||||
|
||||
//Генерация случайной части идентификатора
|
||||
const generateRandomPart = () => {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 10);
|
||||
return `${timestamp}${random}`;
|
||||
};
|
||||
|
||||
//Получение fingerprint для Web
|
||||
const getWebFingerprint = () => {
|
||||
if (typeof navigator === 'undefined' || typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const components = [
|
||||
navigator.userAgent || '',
|
||||
navigator.language || '',
|
||||
navigator.platform || '',
|
||||
new Date().getTimezoneOffset().toString(),
|
||||
window.screen?.width?.toString() || '',
|
||||
window.screen?.height?.toString() || '',
|
||||
window.screen?.colorDepth?.toString() || '',
|
||||
navigator.hardwareConcurrency?.toString() || '',
|
||||
navigator.maxTouchPoints?.toString() || ''
|
||||
];
|
||||
|
||||
return hashString(components.join('|'));
|
||||
};
|
||||
|
||||
//Получение идентификатора для Web с сохранением в localStorage
|
||||
const getWebDeviceId = () => {
|
||||
//Проверяем доступность localStorage через window
|
||||
const storage = typeof window !== 'undefined' ? window.localStorage : null;
|
||||
|
||||
if (!storage) {
|
||||
//localStorage недоступен - генерируем на основе fingerprint
|
||||
const fingerprint = getWebFingerprint();
|
||||
return `WEB-${fingerprint || generateRandomPart()}`;
|
||||
}
|
||||
|
||||
//Пробуем получить сохранённый идентификатор
|
||||
let deviceId = storage.getItem(WEB_DEVICE_ID_KEY);
|
||||
|
||||
if (!deviceId) {
|
||||
//Генерируем новый идентификатор
|
||||
const fingerprint = getWebFingerprint();
|
||||
deviceId = `WEB-${fingerprint}-${generateRandomPart()}`;
|
||||
storage.setItem(WEB_DEVICE_ID_KEY, deviceId);
|
||||
}
|
||||
|
||||
return deviceId;
|
||||
};
|
||||
|
||||
//Получение ANDROID_ID через нативный модуль
|
||||
const getAndroidId = async () => {
|
||||
try {
|
||||
//Пробуем получить через PlatformConstants
|
||||
if (NativeModules.PlatformConstants?.getAndroidID) {
|
||||
const androidId = await NativeModules.PlatformConstants.getAndroidID();
|
||||
|
||||
if (androidId) {
|
||||
return androidId;
|
||||
}
|
||||
}
|
||||
|
||||
//Пробуем получить через другие доступные модули
|
||||
if (NativeModules.RNDeviceInfo?.getAndroidId) {
|
||||
const androidId = await NativeModules.RNDeviceInfo.getAndroidId();
|
||||
|
||||
if (androidId) {
|
||||
return androidId;
|
||||
}
|
||||
}
|
||||
|
||||
//Пробуем получить из констант платформы
|
||||
const constants = Platform.constants || {};
|
||||
|
||||
if (constants.Fingerprint) {
|
||||
return hashString(constants.Fingerprint);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Не удалось получить Android ID:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
//Получение identifierForVendor для iOS
|
||||
const getIosId = async () => {
|
||||
try {
|
||||
//Пробуем получить через доступные нативные модули
|
||||
if (NativeModules.RNDeviceInfo?.getUniqueId) {
|
||||
const uniqueId = await NativeModules.RNDeviceInfo.getUniqueId();
|
||||
|
||||
if (uniqueId) {
|
||||
return uniqueId;
|
||||
}
|
||||
}
|
||||
|
||||
if (NativeModules.SettingsManager?.settings?.AppleLocale) {
|
||||
//Используем комбинацию доступных данных для fingerprint
|
||||
const settings = NativeModules.SettingsManager.settings;
|
||||
const components = [settings.AppleLocale || '', settings.AppleLanguages?.join(',') || '', Platform.constants?.osVersion || ''];
|
||||
|
||||
return hashString(components.join('|'));
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Не удалось получить iOS ID:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация fingerprint для мобильных платформ
|
||||
const getMobileFingerprint = () => {
|
||||
const constants = Platform.constants || {};
|
||||
|
||||
const components = [
|
||||
Platform.OS,
|
||||
Platform.Version?.toString() || '',
|
||||
constants.Brand || '',
|
||||
constants.Model || '',
|
||||
constants.Manufacturer || '',
|
||||
constants.osVersion || '',
|
||||
constants.systemName || ''
|
||||
];
|
||||
|
||||
return hashString(components.filter(Boolean).join('|'));
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Получение постоянного идентификатора устройства
|
||||
const getPersistentDeviceId = async () => {
|
||||
//Web платформа
|
||||
if (Platform.OS === 'web') {
|
||||
return getWebDeviceId();
|
||||
}
|
||||
|
||||
//Android платформа
|
||||
if (Platform.OS === 'android') {
|
||||
const androidId = await getAndroidId();
|
||||
|
||||
if (androidId) {
|
||||
return `AND-${androidId}`.toUpperCase();
|
||||
}
|
||||
|
||||
//Fallback на fingerprint
|
||||
const fingerprint = getMobileFingerprint();
|
||||
return `AND-${fingerprint}-${generateRandomPart()}`.toUpperCase();
|
||||
}
|
||||
|
||||
//iOS платформа
|
||||
if (Platform.OS === 'ios') {
|
||||
const iosId = await getIosId();
|
||||
|
||||
if (iosId) {
|
||||
return `IOS-${iosId}`.toUpperCase();
|
||||
}
|
||||
|
||||
//Fallback на fingerprint
|
||||
const fingerprint = getMobileFingerprint();
|
||||
return `IOS-${fingerprint}-${generateRandomPart()}`.toUpperCase();
|
||||
}
|
||||
|
||||
//Неизвестная платформа
|
||||
return `UNK-${generateRandomPart()}`.toUpperCase();
|
||||
};
|
||||
|
||||
//Проверка доступности постоянного идентификатора
|
||||
const isPersistentIdAvailable = async () => {
|
||||
if (Platform.OS === 'web') {
|
||||
return typeof window !== 'undefined' && !!window.localStorage;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const androidId = await getAndroidId();
|
||||
return !!androidId;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
const iosId = await getIosId();
|
||||
return !!iosId;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
getPersistentDeviceId,
|
||||
isPersistentIdAvailable
|
||||
};
|
||||
22
rn/app/src/utils/loginFormUtils.js
Normal file
22
rn/app/src/utils/loginFormUtils.js
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Утилиты формы входа (логин): видимость полей, правила отображения
|
||||
*/
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Определяет, нужно ли показывать поле ввода адреса сервера
|
||||
function isServerUrlFieldVisible(hideServerUrl, serverUrl) {
|
||||
const hasServerUrl = Boolean(serverUrl && String(serverUrl).trim());
|
||||
return !hideServerUrl || !hasServerUrl;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
isServerUrlFieldVisible
|
||||
};
|
||||
@ -8,15 +8,12 @@
|
||||
//---------------------
|
||||
|
||||
const { Dimensions, Platform, PixelRatio } = require('react-native'); //Размеры экрана и платформа
|
||||
const { BASE_WIDTH, BASE_HEIGHT } = require('../config/responsiveConfig'); //Базовые размеры для адаптивности
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Константы для базовых размеров
|
||||
const BASE_WIDTH = 375; // iPhone 11/12/13/14
|
||||
const BASE_HEIGHT = 812;
|
||||
|
||||
//Получение размеров экрана
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||
|
||||
|
||||
170
rn/app/src/utils/secureStorage.js
Normal file
170
rn/app/src/utils/secureStorage.js
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Утилиты безопасного хранения данных
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const { ENCRYPTED_PREFIX, SECRET_KEY_LENGTH } = require('../config/storageKeys'); //Ключи и константы хранилища
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Вычисление XOR двух байтов через арифметические операции
|
||||
const xorByte = (a, b) => {
|
||||
let result = 0;
|
||||
let bit = 1;
|
||||
|
||||
//Обрабатываем каждый из 8 бит
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const bitA = Math.floor(a / bit) % 2;
|
||||
const bitB = Math.floor(b / bit) % 2;
|
||||
|
||||
//XOR: результат 1 только если биты разные
|
||||
if (bitA !== bitB) {
|
||||
result += bit;
|
||||
}
|
||||
|
||||
bit *= 2;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
//Генерация хэша ключа на основе секретного ключа и соли
|
||||
const generateKeyHash = (secretKey, salt) => {
|
||||
const combined = secretKey + salt;
|
||||
const hash = [];
|
||||
|
||||
for (let i = 0; i < combined.length; i++) {
|
||||
hash.push(combined.charCodeAt(i) % 256);
|
||||
}
|
||||
|
||||
//Расширяем хэш до 256 байт для лучшего распределения
|
||||
while (hash.length < 256) {
|
||||
const newByte = (hash[hash.length - 1] + hash[hash.length - 2] + hash.length) % 256;
|
||||
hash.push(newByte);
|
||||
}
|
||||
|
||||
return hash;
|
||||
};
|
||||
|
||||
//XOR-шифрование строки с использованием ключа
|
||||
const xorEncrypt = (text, keyHash) => {
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const charCode = text.charCodeAt(i);
|
||||
const keyByte = keyHash[i % keyHash.length];
|
||||
result.push(xorByte(charCode, keyByte));
|
||||
}
|
||||
|
||||
//Конвертируем в hex-строку
|
||||
return result.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
//XOR-расшифрование строки
|
||||
const xorDecrypt = (encryptedHex, keyHash) => {
|
||||
const bytes = [];
|
||||
|
||||
//Разбираем hex-строку на байты
|
||||
for (let i = 0; i < encryptedHex.length; i += 2) {
|
||||
bytes.push(parseInt(encryptedHex.substr(i, 2), 16));
|
||||
}
|
||||
|
||||
//Расшифровываем XOR
|
||||
const result = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const keyByte = keyHash[i % keyHash.length];
|
||||
result.push(String.fromCharCode(xorByte(bytes[i], keyByte)));
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Генерация уникального секретного ключа для устройства
|
||||
const generateSecretKey = () => {
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*()-_=+[]{}|;:,.<>?';
|
||||
const timestamp = Date.now().toString(36);
|
||||
let key = '';
|
||||
|
||||
//Генерируем случайную часть ключа
|
||||
for (let i = 0; i < SECRET_KEY_LENGTH; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
key += characters[randomIndex];
|
||||
}
|
||||
|
||||
//Добавляем временную метку для уникальности
|
||||
return `${key}_${timestamp}`;
|
||||
};
|
||||
|
||||
//Шифрование чувствительных данных
|
||||
const encryptData = (data, secretKey, salt = '') => {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!secretKey) {
|
||||
console.error('Ошибка шифрования: секретный ключ не предоставлен');
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const keyHash = generateKeyHash(secretKey, salt);
|
||||
const encrypted = xorEncrypt(data, keyHash);
|
||||
return ENCRYPTED_PREFIX + encrypted;
|
||||
} catch (error) {
|
||||
console.error('Ошибка шифрования:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
//Расшифрование данных
|
||||
const decryptData = (encryptedData, secretKey, salt = '') => {
|
||||
if (!encryptedData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!secretKey) {
|
||||
console.error('Ошибка расшифровки: секретный ключ не предоставлен');
|
||||
return '';
|
||||
}
|
||||
|
||||
//Проверяем префикс
|
||||
if (!encryptedData.startsWith(ENCRYPTED_PREFIX)) {
|
||||
//Данные не зашифрованы - возвращаем как есть (для обратной совместимости)
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
try {
|
||||
const encryptedHex = encryptedData.slice(ENCRYPTED_PREFIX.length);
|
||||
const keyHash = generateKeyHash(secretKey, salt);
|
||||
return xorDecrypt(encryptedHex, keyHash);
|
||||
} catch (error) {
|
||||
console.error('Ошибка расшифровки:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
//Проверка зашифрованы ли данные
|
||||
const isEncrypted = data => {
|
||||
return data && data.startsWith(ENCRYPTED_PREFIX);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
generateSecretKey,
|
||||
encryptData,
|
||||
decryptData,
|
||||
isEncrypted
|
||||
};
|
||||
100
rn/app/src/utils/validation.js
Normal file
100
rn/app/src/utils/validation.js
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
Предрейсовые осмотры - мобильное приложение
|
||||
Универсальные валидаторы для форм (URL, числа и т.д.)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Сообщения об ошибках валидации (единый источник для масштабирования)
|
||||
const VALIDATION_MESSAGES = {
|
||||
SERVER_URL_EMPTY: 'Введите адрес сервера',
|
||||
SERVER_URL_PROTOCOL: 'Используйте http:// или https:// протокол',
|
||||
SERVER_URL_INVALID: 'Некорректный формат адреса сервера',
|
||||
SERVER_URL_INVALID_SHORT: 'Некорректный формат URL',
|
||||
IDLE_TIMEOUT_EMPTY: 'Введите время простоя',
|
||||
IDLE_TIMEOUT_MIN: 'Введите положительное число (минимум 1 минута)',
|
||||
IDLE_TIMEOUT_MAX: 'Максимальное значение: 1440 минут (24 часа)'
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Нормализация URL сервера для сравнения (без завершающих слэшей)
|
||||
function normalizeServerUrl(url) {
|
||||
if (!url || typeof url !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return url.trim().replace(/\/+$/, '') || '';
|
||||
}
|
||||
|
||||
//Валидация URL сервера. Возвращает true при успехе, иначе строку с ошибкой
|
||||
function validateServerUrl(url, options) {
|
||||
const opts = options || {};
|
||||
const emptyMessage = opts.emptyMessage || VALIDATION_MESSAGES.SERVER_URL_EMPTY;
|
||||
const invalidMessage = opts.invalidMessage || VALIDATION_MESSAGES.SERVER_URL_INVALID_SHORT;
|
||||
const allowEmpty = opts.allowEmpty === true;
|
||||
|
||||
if (!url || !String(url).trim()) {
|
||||
if (allowEmpty) {
|
||||
return true;
|
||||
}
|
||||
return emptyMessage;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(String(url).trim());
|
||||
if (!parsedUrl.protocol.startsWith('http')) {
|
||||
return VALIDATION_MESSAGES.SERVER_URL_PROTOCOL;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return invalidMessage;
|
||||
}
|
||||
}
|
||||
|
||||
//Валидация URL сервера с допуском пустого значения
|
||||
function validateServerUrlAllowEmpty(url) {
|
||||
return validateServerUrl(url, { allowEmpty: true });
|
||||
}
|
||||
|
||||
//Валидация времени простоя (минуты). Возвращает true при успехе, иначе строку с ошибкой
|
||||
function validateIdleTimeout(value, options) {
|
||||
const opts = options || {};
|
||||
const min = opts.min !== undefined ? opts.min : 1;
|
||||
const max = opts.max !== undefined ? opts.max : 1440;
|
||||
const emptyMessage = opts.emptyMessage || VALIDATION_MESSAGES.IDLE_TIMEOUT_EMPTY;
|
||||
const minMessage = opts.minMessage || VALIDATION_MESSAGES.IDLE_TIMEOUT_MIN;
|
||||
const maxMessage = opts.maxMessage || VALIDATION_MESSAGES.IDLE_TIMEOUT_MAX;
|
||||
|
||||
if (!value || !String(value).trim()) {
|
||||
return emptyMessage;
|
||||
}
|
||||
|
||||
const num = parseInt(String(value).trim(), 10);
|
||||
if (isNaN(num) || num < min) {
|
||||
return minMessage;
|
||||
}
|
||||
if (num > max) {
|
||||
return maxMessage;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
module.exports = {
|
||||
VALIDATION_MESSAGES,
|
||||
normalizeServerUrl,
|
||||
validateServerUrl,
|
||||
validateServerUrlAllowEmpty,
|
||||
validateIdleTimeout
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user