add compomemts&front
This commit is contained in:
@@ -1638,3 +1638,23 @@
|
|||||||
[2026-08-09T19:34:39.791Z] [INFO] Card logs initialized successfully
|
[2026-08-09T19:34:39.791Z] [INFO] Card logs initialized successfully
|
||||||
[2026-08-09T19:36:51.312Z] [INFO] Server running on http://localhost:3005
|
[2026-08-09T19:36:51.312Z] [INFO] Server running on http://localhost:3005
|
||||||
[2026-08-09T19:36:51.318Z] [INFO] Card logs initialized successfully
|
[2026-08-09T19:36:51.318Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T07:16:29.299Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T07:16:29.302Z] [INFO] Card log file created at: D:\rasch_git\electron\backend\logs\cards.log
|
||||||
|
[2026-08-10T07:16:29.303Z] [INFO] Card log file created at: D:\rasch_git\electron\backend\logs\cards.log
|
||||||
|
[2026-08-10T07:16:29.303Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T07:16:37.113Z] [ERROR] Error: ENOENT: no such file or directory, stat 'D:\rasch_git\electron\admin-frontend\build\index.html'
|
||||||
|
[2026-08-10T07:17:35.578Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T07:17:35.581Z] [INFO] Card logs initialized successfully
|
||||||
|
[2026-08-10T07:17:43.099Z] [INFO] Login successful
|
||||||
|
[2026-08-10T07:17:43.218Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
|
||||||
|
[2026-08-10T07:17:43.250Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
|
||||||
|
[2026-08-10T07:17:44.412Z] [WARN] Log file not found: D:\rasch_git\electron\backend\logs\app.log
|
||||||
|
[2026-08-10T07:18:02.846Z] [INFO] [LOGIN] Попытка входа: grivova
|
||||||
|
[2026-08-10T07:18:02.846Z] [INFO] [LOGIN] Проверка подключения к базе...
|
||||||
|
[2026-08-10T07:18:02.852Z] [INFO] [LOGIN] Результат поиска пользователя: найден
|
||||||
|
[2026-08-10T07:18:02.853Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
|
||||||
|
[2026-08-10T07:18:02.943Z] [INFO] [LOGIN] Результат сравнения пароля: true
|
||||||
|
[2026-08-10T07:18:02.944Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":3,"username":"grivova"}
|
||||||
|
[2026-08-10T07:18:02.945Z] [INFO] [LOGIN] Ответ отправлен
|
||||||
|
[2026-08-10T07:22:25.057Z] [INFO] Server running on http://localhost:3005
|
||||||
|
[2026-08-10T07:22:25.060Z] [INFO] Card logs initialized successfully
|
||||||
|
|||||||
Generated
-1
@@ -7,7 +7,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "admin-server",
|
"name": "admin-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"electron": "electron .",
|
"electron": "electron .",
|
||||||
"electron-dev": "set NODE_ENV=development&& electron .",
|
"electron-dev": "set NODE_ENV=development&& electron .",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"payslipsPath": "\\\\PC-20010\\payslips",
|
"payslipsPath": "\\\\PC-20010\\payslips2",
|
||||||
"server": {
|
"server": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": "3001"
|
"port": "3001"
|
||||||
|
|||||||
Generated
+588
-700
File diff suppressed because it is too large
Load Diff
+16
-12
@@ -9,13 +9,17 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"pdfjs-dist": "^4.10.38",
|
"framer-motion": "^13.0.0",
|
||||||
|
"pdfjs-dist": "^3.11.174",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-pdf": "^10.0.0",
|
"react-icons": "^4.11.0",
|
||||||
"serialport": "^13.0.0",
|
"react-pdf": "^8.0.0",
|
||||||
|
"react-zoom-pan-pinch": "^3.3.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@@ -40,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "node_modules/pdfjs-dist/build/pdf.worker.min.mjs",
|
"from": "node_modules/pdfjs-dist/build/pdf.worker.min.js",
|
||||||
"to": "pdf.worker.mjs",
|
"to": "pdf.worker.mjs",
|
||||||
"filter": [
|
"filter": [
|
||||||
"**/*"
|
"**/*"
|
||||||
@@ -69,13 +73,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^26.2.0",
|
"@types/node": "^26.2.0",
|
||||||
"@vitejs/plugin-react":"^6.0.5",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"concurrently":"^7.5.0",
|
"concurrently": "^7.5.0",
|
||||||
"electron":"^37.0.0",
|
"electron": "^37.0.0",
|
||||||
"electron-builder":"^26.15.3",
|
"electron-builder": "^26.15.3",
|
||||||
"electron-rebuild":"^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
"vite":"^8.2.1",
|
"vite": "^8.2.1",
|
||||||
"vitest":"^2.1.8",
|
"vitest": "^2.1.8",
|
||||||
"wait-on":"^6.0.1"
|
"wait-on": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
@@ -5,7 +5,6 @@ const isDev = require('electron-is-dev');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const COM_PORT = process.env.COM_PORT;
|
const COM_PORT = process.env.COM_PORT;
|
||||||
const COM_SPEED = process.env.COM_SPEED;
|
const COM_SPEED = process.env.COM_SPEED;
|
||||||
const { SerialPort } = require('serialport');
|
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { logger } = require('../../backend/src/config/logger');
|
const { logger } = require('../../backend/src/config/logger');
|
||||||
const config = require('../src/config.json')
|
const config = require('../src/config.json')
|
||||||
@@ -140,57 +139,6 @@ app.on('will-quit', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//считыватель
|
//считыватель
|
||||||
SerialPort.list().then(ports => {
|
|
||||||
logger.info('Доступные порты:', ports.map(p => p.path));
|
|
||||||
}).catch(err => {
|
|
||||||
logger.error('Ошибка при получении списка портов:', err);
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const port = new SerialPort({ path: COM_PORT, baudRate: parseInt(COM_SPEED, 10)});
|
|
||||||
let buffer = '';
|
|
||||||
port.on('open', () => {
|
|
||||||
logger.info(' Serial port открыт');
|
|
||||||
});
|
|
||||||
port.on('data', (data) => {
|
|
||||||
logger.info('RAW DATA:', data);
|
|
||||||
buffer += data.toString('utf8');
|
|
||||||
if (buffer.includes('\n')) {
|
|
||||||
const lines = buffer.split('\n');
|
|
||||||
lines.forEach((line, idx) => {
|
|
||||||
if (idx < lines.length - 1) {
|
|
||||||
const uid = line.trim().replace(/\D/g, '');
|
|
||||||
if (uid) {
|
|
||||||
console.log(`UID карты: ${uid}`);
|
|
||||||
console.log('[DEBUG] Отправляю событие на admin-server:', {
|
|
||||||
event: 'CARD_PLACED',
|
|
||||||
uid: uid,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
axios.post(`${config.adminUrl}}/api/card-event`, {
|
|
||||||
event: 'CARD_PLACED',
|
|
||||||
uid: uid,
|
|
||||||
timestamp: Date.now()
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('[ADMIN][CARD_LOG] Ошибка отправки:', err.message);
|
|
||||||
});
|
|
||||||
if (mainWindow && mainWindow.webContents) {
|
|
||||||
mainWindow.webContents.send('card-detected', uid);
|
|
||||||
console.log(` Отправка события 'card-detected' в рендерер с UID: ${uid}`);
|
|
||||||
} else {
|
|
||||||
console.warn('mainWindow или webContents не определены, событие не отправлено');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buffer = lines[lines.length - 1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
port.on('error', (err) => {
|
|
||||||
console.error('Ошибка serialport:', err);
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('!!! FATAL: FAILED TO INITIALIZE SERIALPORT !!!', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('get-app-config', async (event) => {
|
ipcMain.handle('get-app-config', async (event) => {
|
||||||
console.log('[IPC] Renderer requested app config. Sending:', appConfig);
|
console.log('[IPC] Renderer requested app config. Sending:', appConfig);
|
||||||
|
|||||||
+275
-23
@@ -1,30 +1,111 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import './styles/main.css';
|
import './styles/main.css';
|
||||||
|
import './styles/animations.css';
|
||||||
import LoginWindow from './components/LoginWindow';
|
import LoginWindow from './components/LoginWindow';
|
||||||
import EmployeeInfo from './components/EmployeeInfo';
|
|
||||||
import GuestMode from './components/GuestMode';
|
import GuestMode from './components/GuestMode';
|
||||||
|
import MainMenu from './components/MenuComponent';
|
||||||
|
import VideoBackground from './components/VideoBackground';
|
||||||
|
import BottomBar from './components/BottomBar';
|
||||||
import PayslipViewer from './components/PayslipViewer';
|
import PayslipViewer from './components/PayslipViewer';
|
||||||
|
import EmployeeInfo from './components/EmployeeInfo';
|
||||||
|
import { useActivityTimer } from './components/useActivityTimer'
|
||||||
import config from './config.json';
|
import config from './config.json';
|
||||||
|
|
||||||
|
// Импорты разделов
|
||||||
|
import Finance from './components/Finance';
|
||||||
|
import Medicine from './components/Medicine';
|
||||||
|
import TradeUnion from './components/TradeUnion';
|
||||||
|
import LaborSafety from './components/LaborSafety';
|
||||||
|
import Contacts from './components/Contacts';
|
||||||
|
import News from './components/News';
|
||||||
|
import Profile from './components/Profile';
|
||||||
|
|
||||||
interface Employee {
|
interface Employee {
|
||||||
id: string;
|
id: string;
|
||||||
tnom: string;
|
tnom: string;
|
||||||
famaly: string;
|
famaly: string;
|
||||||
ima: string;
|
ima: string;
|
||||||
otch: string;
|
otch: string;
|
||||||
category: string;
|
category?: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
identNumber: string;
|
identNumber?: string;
|
||||||
|
departmentName?: string;
|
||||||
|
profession?: string;
|
||||||
|
cardNumber?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
// 📊 Состояния приложения
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [isGuestMode, setIsGuestMode] = useState(false);
|
const [isGuestMode, setIsGuestMode] = useState(false);
|
||||||
const [employee, setEmployee] = useState<Employee | null>(null);
|
const [employee, setEmployee] = useState<Employee | null>(null);
|
||||||
|
const [loginError, setLoginError] = useState('');
|
||||||
|
const [showVideo, setShowVideo] = useState(true);
|
||||||
|
const [currentSection, setCurrentSection] = useState<string | null>(null);
|
||||||
const [showPayslip, setShowPayslip] = useState(false);
|
const [showPayslip, setShowPayslip] = useState(false);
|
||||||
const [selectedPeriod, setSelectedPeriod] = useState('');
|
const [selectedPeriod, setSelectedPeriod] = useState('');
|
||||||
const [loginError, setLoginError] = useState('');
|
|
||||||
|
|
||||||
|
// 📊 Состояния для таймера
|
||||||
|
const [showInactiveWarning, setShowInactiveWarning] = useState(false);
|
||||||
|
|
||||||
|
// 📌 Реф для поля ввода логина (исправленный тип)
|
||||||
|
const loginInputRef = useRef<HTMLInputElement>(null!); // ✅ Исправлено
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🛑 Функция ПОЛНОГО сброса приложения (как при запуске)
|
||||||
|
// ============================================
|
||||||
|
const resetToInitialState = () => {
|
||||||
|
console.log('🔄 ПОЛНЫЙ СБРОС ПРИЛОЖЕНИЯ — как при запуске');
|
||||||
|
|
||||||
|
// ✅ Сбрасываем ВСЕ состояния
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setIsGuestMode(false);
|
||||||
|
setEmployee(null);
|
||||||
|
setCurrentSection(null);
|
||||||
|
setShowPayslip(false);
|
||||||
|
setSelectedPeriod('');
|
||||||
|
setShowInactiveWarning(false);
|
||||||
|
setLoginError('');
|
||||||
|
|
||||||
|
// ✅ ПОКАЗЫВАЕМ ВИДЕО-ЗАСТАВКУ СНАЧАЛА
|
||||||
|
setShowVideo(true);
|
||||||
|
|
||||||
|
// ✅ Через 200ms ставим фокус на поле ввода (после видео)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (loginInputRef.current) {
|
||||||
|
loginInputRef.current.focus();
|
||||||
|
console.log('✅ Фокус на поле ввода логина');
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🕐 Таймер неактивности (работает ВСЕГДА)
|
||||||
|
// ============================================
|
||||||
|
const { isWarning, timeLeft, resetTimer } = useActivityTimer({
|
||||||
|
timeout: 30000, // 30 секунд до предупреждения
|
||||||
|
warningTimeout: 10000, // 10 секунд до сброса
|
||||||
|
onInactive: () => {
|
||||||
|
console.log('🛑 Пользователь неактивен! ПОЛНЫЙ СБРОС ПРИЛОЖЕНИЯ');
|
||||||
|
resetToInitialState();
|
||||||
|
},
|
||||||
|
onWarning: () => {
|
||||||
|
console.log('⚠️ Предупреждение о неактивности!');
|
||||||
|
setShowInactiveWarning(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 📌 Функция для сброса таймера при любом действии
|
||||||
|
const handleUserActivity = () => {
|
||||||
|
if (showInactiveWarning) {
|
||||||
|
setShowInactiveWarning(false);
|
||||||
|
}
|
||||||
|
resetTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🔐 Вход в систему
|
||||||
|
// ============================================
|
||||||
const handleLogin = async (id: string) => {
|
const handleLogin = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const isCardUid = /^\d{7}$/.test(id);
|
const isCardUid = /^\d{7}$/.test(id);
|
||||||
@@ -37,6 +118,7 @@ function App() {
|
|||||||
setEmployee(data);
|
setEmployee(data);
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setLoginError('');
|
setLoginError('');
|
||||||
|
handleUserActivity();
|
||||||
} else {
|
} else {
|
||||||
setLoginError('Сотрудник не найден');
|
setLoginError('Сотрудник не найден');
|
||||||
}
|
}
|
||||||
@@ -46,52 +128,222 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 👤 Режим гостя
|
||||||
const handleGuestMode = () => {
|
const handleGuestMode = () => {
|
||||||
setIsGuestMode(true);
|
setIsGuestMode(true);
|
||||||
|
handleUserActivity();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ↩️ Выход из системы
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setIsLoggedIn(false);
|
resetToInitialState();
|
||||||
setEmployee(null);
|
};
|
||||||
setIsGuestMode(false);
|
|
||||||
|
// ↩️ Возврат в главное меню
|
||||||
|
const handleBackToMenu = () => {
|
||||||
|
setCurrentSection(null);
|
||||||
setShowPayslip(false);
|
setShowPayslip(false);
|
||||||
setSelectedPeriod('');
|
setSelectedPeriod('');
|
||||||
|
handleUserActivity();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 📂 Выбор раздела
|
||||||
|
const handleSectionSelect = (section: string) => {
|
||||||
|
setCurrentSection(section);
|
||||||
|
handleUserActivity();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 📄 Просмотр расчетного листка
|
||||||
const handleViewPayslip = (period: string) => {
|
const handleViewPayslip = (period: string) => {
|
||||||
setSelectedPeriod(period);
|
setSelectedPeriod(period);
|
||||||
setShowPayslip(true);
|
setShowPayslip(true);
|
||||||
|
handleUserActivity();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ↩️ Назад из расчетного листка
|
||||||
const handlePayslipBack = () => {
|
const handlePayslipBack = () => {
|
||||||
setShowPayslip(false);
|
setShowPayslip(false);
|
||||||
setSelectedPeriod('');
|
setSelectedPeriod('');
|
||||||
|
handleUserActivity();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🔗 URL для расчетного листка
|
||||||
const getPayslipUrl = (employee: Employee, period: string) => {
|
const getPayslipUrl = (employee: Employee, period: string) => {
|
||||||
const fileName = `${employee.tnom}.pdf`;
|
const fileName = `${employee.tnom}.pdf`;
|
||||||
|
|
||||||
return `${config.backendUrl}/api/payslip/${period}/${fileName}`;
|
return `${config.backendUrl}/api/payslip/${period}/${fileName}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// ============================================
|
||||||
<div className="app">
|
// ⚠️ Предупреждение о неактивности
|
||||||
{!isLoggedIn && !isGuestMode ? (
|
// ============================================
|
||||||
<LoginWindow onLogin={handleLogin} onGuestMode={handleGuestMode} loginError={loginError} />
|
const renderWarning = () => {
|
||||||
) : isGuestMode ? (
|
if (!showInactiveWarning) return null;
|
||||||
<GuestMode onBack={handleBack} />
|
|
||||||
) : showPayslip ? (
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 9999,
|
||||||
|
backdropFilter: 'blur(5px)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255, 193, 7, 0.98)',
|
||||||
|
padding: '35px 45px',
|
||||||
|
borderRadius: '20px',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||||
|
textAlign: 'center',
|
||||||
|
maxWidth: '400px',
|
||||||
|
animation: 'pulseWarning 1s ease-in-out infinite',
|
||||||
|
border: '3px solid #ff6b6b',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '48px', marginBottom: '10px' }}>⚠️</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '22px' }}>
|
||||||
|
Сессия истекает!
|
||||||
|
</h3>
|
||||||
|
<p style={{ margin: '0 0 5px 0', color: '#636e72', fontSize: '14px' }}>
|
||||||
|
Вы были неактивны. Приложение будет сброшено через:
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
margin: '10px 0 20px 0',
|
||||||
|
fontSize: '48px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#d32f2f',
|
||||||
|
animation: 'blinkText 0.5s ease-in-out infinite'
|
||||||
|
}}>
|
||||||
|
{Math.ceil(timeLeft / 1000)}с
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
color: '#636e72',
|
||||||
|
fontSize: '13px',
|
||||||
|
margin: '0 0 15px 0',
|
||||||
|
opacity: 0.7
|
||||||
|
}}>
|
||||||
|
👆 Нажмите или начните двигать мышью для отмены
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowInactiveWarning(false);
|
||||||
|
handleUserActivity();
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: '14px 45px',
|
||||||
|
background: '#28a745',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '12px',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '18px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
boxShadow: '0 4px 15px rgba(40,167,69,0.3)',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#218838';
|
||||||
|
e.currentTarget.style.transform = 'scale(1.05)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#28a745';
|
||||||
|
e.currentTarget.style.transform = 'scale(1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Продолжить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 📄 Рендер контента
|
||||||
|
// ============================================
|
||||||
|
const renderContent = () => {
|
||||||
|
// 🎬 Если showVideo = true — показываем видео
|
||||||
|
if (showVideo) {
|
||||||
|
return <VideoBackground onComplete={() => setShowVideo(false)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Если не залогинен и не гость — показываем окно входа
|
||||||
|
if (!isLoggedIn && !isGuestMode) {
|
||||||
|
return (
|
||||||
|
<LoginWindow
|
||||||
|
onLogin={handleLogin}
|
||||||
|
onGuestMode={handleGuestMode}
|
||||||
|
loginError={loginError}
|
||||||
|
inputRef={loginInputRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👤 Режим гостя
|
||||||
|
if (isGuestMode) {
|
||||||
|
return <GuestMode onBack={handleBack} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📄 Просмотр расчетного листка
|
||||||
|
if (showPayslip) {
|
||||||
|
return (
|
||||||
<PayslipViewer
|
<PayslipViewer
|
||||||
fileUrl={employee && selectedPeriod ? getPayslipUrl(employee, selectedPeriod) : "/payslips/test.pdf"}
|
fileUrl={employee && selectedPeriod ? getPayslipUrl(employee, selectedPeriod) : "/payslips/test.pdf"}
|
||||||
onBack={handlePayslipBack}
|
onBack={handlePayslipBack}
|
||||||
/>
|
/>
|
||||||
) : employee ? (
|
);
|
||||||
<EmployeeInfo
|
}
|
||||||
employee={employee}
|
|
||||||
onViewPayslip={handleViewPayslip}
|
// 📂 Разделы приложения
|
||||||
onBack={handleBack}
|
if (currentSection) {
|
||||||
/>
|
switch (currentSection) {
|
||||||
) : null}
|
case 'employee-info':
|
||||||
|
return <EmployeeInfo employee={employee!} onViewPayslip={handleViewPayslip} onBack={handleBackToMenu} />;
|
||||||
|
case 'finance':
|
||||||
|
return <Finance onBack={handleBackToMenu} onViewPayslip={handleViewPayslip} employee={employee!} />;
|
||||||
|
case 'medicine':
|
||||||
|
return <Medicine onBack={handleBackToMenu} employee={employee!} />;
|
||||||
|
case 'trade-union':
|
||||||
|
return <TradeUnion onBack={handleBackToMenu} employee={employee!} />;
|
||||||
|
case 'labor-safety':
|
||||||
|
return <LaborSafety onBack={handleBackToMenu} employee={employee!} />;
|
||||||
|
case 'contacts':
|
||||||
|
return <Contacts onBack={handleBackToMenu} />;
|
||||||
|
case 'news':
|
||||||
|
return <News onBack={handleBackToMenu} />;
|
||||||
|
case 'profile':
|
||||||
|
return <Profile onBack={handleBackToMenu} employee={employee!} />;
|
||||||
|
default:
|
||||||
|
return <MainMenu onSectionSelect={handleSectionSelect} employee={employee!} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📋 Главное меню
|
||||||
|
return <MainMenu onSectionSelect={handleSectionSelect} employee={employee!} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 🚀 Рендер приложения
|
||||||
|
// ============================================
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="app"
|
||||||
|
onClick={handleUserActivity}
|
||||||
|
onKeyDown={handleUserActivity}
|
||||||
|
onMouseMove={handleUserActivity}
|
||||||
|
onTouchStart={handleUserActivity}
|
||||||
|
onScroll={handleUserActivity}
|
||||||
|
onWheel={handleUserActivity}
|
||||||
|
>
|
||||||
|
{/* ⚠️ Предупреждение о неактивности */}
|
||||||
|
{renderWarning()}
|
||||||
|
|
||||||
|
{/* 📄 Основной контент */}
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
{/* 📱 Нижняя панель (только для авторизованных) */}
|
||||||
|
{isLoggedIn && !showPayslip && <BottomBar />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface CurrencyRate {
|
||||||
|
currency: string;
|
||||||
|
rate: number;
|
||||||
|
change: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetalRate {
|
||||||
|
metal: string;
|
||||||
|
price: number;
|
||||||
|
change: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomBar: React.FC = () => {
|
||||||
|
const [currencyRates, setCurrencyRates] = useState<CurrencyRate[]>([
|
||||||
|
{ currency: 'USD', rate: 3.22, change: 0.5 },
|
||||||
|
{ currency: 'EUR', rate: 3.45, change: -0.2 },
|
||||||
|
{ currency: 'CNY', rate: 0.44, change: 0.1 },
|
||||||
|
{ currency: 'RUB', rate: 0.035, change: -0.3 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [metalRates, setMetalRates] = useState<MetalRate[]>([
|
||||||
|
{ metal: 'Золото', price: 1950, change: 0.8 },
|
||||||
|
{ metal: 'Серебро', price: 23.5, change: -0.5 },
|
||||||
|
{ metal: 'Платина', price: 920, change: 0.3 },
|
||||||
|
{ metal: 'Палладий', price: 1450, change: -0.7 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date()); // ✅ Добавлено
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const getAllItems = () => {
|
||||||
|
const currencyItems = currencyRates.map(item => ({
|
||||||
|
id: `currency-${item.currency}`,
|
||||||
|
type: 'currency' as const,
|
||||||
|
label: item.currency,
|
||||||
|
value: item.rate,
|
||||||
|
change: item.change,
|
||||||
|
displayValue: item.rate.toFixed(item.rate < 1 ? 3 : 2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const metalItems = metalRates.map(item => ({
|
||||||
|
id: `metal-${item.metal}`,
|
||||||
|
type: 'metal' as const,
|
||||||
|
label: item.metal,
|
||||||
|
value: item.price,
|
||||||
|
change: item.change,
|
||||||
|
displayValue: `$${item.price.toFixed(item.price < 10 ? 2 : 0)}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...currencyItems, ...metalItems];
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = getAllItems();
|
||||||
|
const doubledItems = [...items, ...items, ...items];
|
||||||
|
|
||||||
|
// ✅ Обновление времени каждую секунду
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentTime(new Date());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOnline = () => setIsOnline(true);
|
||||||
|
const handleOffline = () => setIsOnline(false);
|
||||||
|
|
||||||
|
window.addEventListener('online', handleOnline);
|
||||||
|
window.addEventListener('offline', handleOffline);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('online', handleOnline);
|
||||||
|
window.removeEventListener('offline', handleOffline);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadRates = async () => {
|
||||||
|
try {
|
||||||
|
if (isOnline) {
|
||||||
|
const response = await fetch('http://localhost:3001/api/rates');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setCurrencyRates(data.currency || currencyRates);
|
||||||
|
setMetalRates(data.metals || metalRates);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const stored = localStorage.getItem('currencyRates');
|
||||||
|
if (stored) {
|
||||||
|
const data = JSON.parse(stored);
|
||||||
|
setCurrencyRates(data.currency || currencyRates);
|
||||||
|
setMetalRates(data.metals || metalRates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Ошибка загрузки курсов:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadRates();
|
||||||
|
const interval = setInterval(loadRates, 60000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isOnline]);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => setIsPaused(true);
|
||||||
|
const handleMouseLeave = () => setIsPaused(false);
|
||||||
|
const handleTouchStart = () => setIsPaused(true);
|
||||||
|
const handleTouchEnd = () => setIsPaused(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bottom-bar"
|
||||||
|
initial={{ y: 100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
background: 'rgba(44, 62, 80, 0.95)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '8px 20px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '16px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
zIndex: 1000,
|
||||||
|
height: '44px',
|
||||||
|
borderTop: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{/* Статус */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
flexShrink: 0,
|
||||||
|
zIndex: 2,
|
||||||
|
background: 'rgba(44, 62, 80, 0.95)',
|
||||||
|
paddingRight: '12px',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
color: isOnline ? '#2ecc71' : '#e74c3c',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}>
|
||||||
|
{isOnline ? '●' : '○'}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontWeight: 600, opacity: 0.7, fontSize: '11px' }}>КУРСЫ</span>
|
||||||
|
<div style={{
|
||||||
|
width: '1px',
|
||||||
|
height: '24px',
|
||||||
|
background: 'rgba(255,255,255,0.15)',
|
||||||
|
marginLeft: '4px',
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Бегущая строка */}
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
maskImage: 'linear-gradient(to right, transparent, black 3%, black 97%, transparent)',
|
||||||
|
WebkitMaskImage: 'linear-gradient(to right, transparent, black 3%, black 97%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
x: isPaused ? 0 : ['0%', '-33.33%'],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 35,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: 'linear',
|
||||||
|
repeatType: 'loop',
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '40px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
willChange: 'transform',
|
||||||
|
transform: 'translateZ(0)',
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
textRendering: 'geometricPrecision',
|
||||||
|
paddingRight: '40px',
|
||||||
|
height: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{doubledItems.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={`${item.id}-${index}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flexShrink: 0,
|
||||||
|
fontSmooth: 'always',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
textRendering: 'geometricPrecision',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'rgba(255,255,255,0.9)',
|
||||||
|
fontSize: '13px',
|
||||||
|
letterSpacing: '0.3px',
|
||||||
|
}}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
fontSize: '13px',
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
}}>
|
||||||
|
{item.displayValue}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
color: item.change >= 0 ? '#2ecc71' : '#e74c3c',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 500,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '2px',
|
||||||
|
letterSpacing: '0.2px',
|
||||||
|
}}>
|
||||||
|
{item.change >= 0 ? '▲' : '▼'}
|
||||||
|
{Math.abs(item.change).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
{item.type === 'metal' && (
|
||||||
|
<span style={{
|
||||||
|
fontSize: '9px',
|
||||||
|
opacity: 0.3,
|
||||||
|
marginLeft: '-2px',
|
||||||
|
}}>
|
||||||
|
$
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ✅ Время с СЕКУНДАМИ и обновлением */}
|
||||||
|
<div style={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
fontSize: '11px',
|
||||||
|
opacity: 0.4,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flexShrink: 0,
|
||||||
|
paddingLeft: '12px',
|
||||||
|
background: 'rgba(44, 62, 80, 0.95)',
|
||||||
|
zIndex: 2,
|
||||||
|
fontSmooth: 'always',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
}}>
|
||||||
|
{currentTime.toLocaleString('ru-RU', {
|
||||||
|
timeZone: 'Europe/Minsk',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Пауза */}
|
||||||
|
{isPaused && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: 'rgba(255,255,255,0.3)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 3,
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
padding: '2px 12px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backdropFilter: 'blur(5px)',
|
||||||
|
}}>
|
||||||
|
⏸ Пауза
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BottomBar;
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { FaArrowLeft } from 'react-icons/fa';
|
||||||
|
import MapViewer from './MapViewer';
|
||||||
|
|
||||||
|
interface ContactsProps {
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Contacts: React.FC<ContactsProps> = ({ onBack }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [activeTab, setActiveTab] = useState<'map' | 'directory'>('directory');
|
||||||
|
const [showMapViewer, setShowMapViewer] = useState(false);
|
||||||
|
|
||||||
|
// ✅ Путь к TIFF-файлу (вместо PDF)
|
||||||
|
const MAP_FILE_URL = '/assets/maps/enterprise-map.tiff';
|
||||||
|
|
||||||
|
const directoryData = [
|
||||||
|
{ name: 'Иванов Иван Иванович', position: 'Директор', phone: '123-45-67', department: 'Руководство' },
|
||||||
|
{ name: 'Петров Петр Петрович', position: 'Главный инженер', phone: '123-45-68', department: 'Руководство' },
|
||||||
|
{ name: 'Сидорова Мария Сергеевна', position: 'Бухгалтер', phone: '123-45-69', department: 'Бухгалтерия' },
|
||||||
|
{ name: 'Козлов Андрей Владимирович', position: 'Начальник цеха', phone: '123-45-70', department: 'Производство' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const emergencyNumbers = [
|
||||||
|
{ name: 'Пожарная служба', phone: '101', icon: '🧯' },
|
||||||
|
{ name: 'Милиция', phone: '102', icon: '🛡️' },
|
||||||
|
{ name: 'Скорая помощь', phone: '103', icon: '🚑' },
|
||||||
|
{ name: 'Газовая служба', phone: '104', icon: '📞' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredData = directoryData.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.position.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.department.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenMap = () => {
|
||||||
|
setShowMapViewer(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMapBack = () => {
|
||||||
|
setShowMapViewer(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showMapViewer) {
|
||||||
|
return (
|
||||||
|
<MapViewer
|
||||||
|
fileUrl={MAP_FILE_URL}
|
||||||
|
onBack={handleMapBack}
|
||||||
|
title="Карта предприятия"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="contacts-section"
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
paddingBottom: '80px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Верхняя панель */}
|
||||||
|
<div className="top-bar" style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '25px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '10px'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#5a6268';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#6c757d';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
<h2 style={{
|
||||||
|
margin: 0,
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 300,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}>
|
||||||
|
📘 Контакты
|
||||||
|
</h2>
|
||||||
|
<div style={{ width: '100px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Карточка карты */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ y: -5, boxShadow: '0 8px 30px rgba(46, 204, 113, 0.3)' }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
onClick={handleOpenMap}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #2ecc71, #27ae60)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '25px 30px',
|
||||||
|
marginBottom: '25px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
boxShadow: '0 4px 20px rgba(46, 204, 113, 0.25)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
minHeight: '80px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '20px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '36px',
|
||||||
|
color: 'white',
|
||||||
|
}}>
|
||||||
|
🗺️
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 style={{
|
||||||
|
margin: '0 0 5px 0',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
Карта предприятия
|
||||||
|
</h3>
|
||||||
|
<p style={{
|
||||||
|
margin: 0,
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}>
|
||||||
|
Схема проезда, план здания, расположение корпусов
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '20px',
|
||||||
|
opacity: 0.8,
|
||||||
|
}}>
|
||||||
|
→
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Вкладки */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
marginBottom: '25px',
|
||||||
|
borderBottom: '2px solid #f0f0f0',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('directory')}
|
||||||
|
style={{
|
||||||
|
padding: '10px 24px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: activeTab === 'directory' ? 600 : 400,
|
||||||
|
color: activeTab === 'directory' ? '#6c5ce7' : '#636e72',
|
||||||
|
borderBottom: activeTab === 'directory' ? '3px solid #6c5ce7' : '3px solid transparent',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
📘 Справочник
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('map')}
|
||||||
|
style={{
|
||||||
|
padding: '10px 24px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: activeTab === 'map' ? 600 : 400,
|
||||||
|
color: activeTab === 'map' ? '#6c5ce7' : '#636e72',
|
||||||
|
borderBottom: activeTab === 'map' ? '3px solid #6c5ce7' : '3px solid transparent',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗺️ Карта
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Контент вкладок */}
|
||||||
|
{activeTab === 'directory' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
{/* Экстренные номера */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||||
|
gap: '12px',
|
||||||
|
marginBottom: '25px',
|
||||||
|
}}>
|
||||||
|
{emergencyNumbers.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
style={{
|
||||||
|
background: '#e74c3c',
|
||||||
|
color: 'white',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '0 4px 15px rgba(231, 76, 60, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '24px', marginBottom: '5px' }}>{item.icon}</div>
|
||||||
|
<div style={{ fontSize: '13px', fontWeight: 500 }}>{item.name}</div>
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 700 }}>{item.phone}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Поиск */}
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '20px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
borderRadius: '10px',
|
||||||
|
padding: '8px 16px',
|
||||||
|
border: '2px solid #dfe6e9',
|
||||||
|
}}>
|
||||||
|
🔍
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск по справочнику..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
padding: '10px 0',
|
||||||
|
fontSize: '15px',
|
||||||
|
outline: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Таблица */}
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead style={{ background: '#f8f9fa', borderBottom: '2px solid #f1f2f6' }}>
|
||||||
|
<tr>
|
||||||
|
<th style={{ padding: '15px 20px', textAlign: 'left', fontSize: '13px', color: '#636e72' }}>ФИО</th>
|
||||||
|
<th style={{ padding: '15px 20px', textAlign: 'left', fontSize: '13px', color: '#636e72' }}>Должность</th>
|
||||||
|
<th style={{ padding: '15px 20px', textAlign: 'left', fontSize: '13px', color: '#636e72' }}>Телефон</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredData.map((item, index) => (
|
||||||
|
<motion.tr
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid #f1f2f6',
|
||||||
|
transition: 'background 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = '#f8f9fa'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
|
||||||
|
>
|
||||||
|
<td style={{ padding: '12px 20px', fontSize: '14px' }}>{item.name}</td>
|
||||||
|
<td style={{ padding: '12px 20px', fontSize: '14px', color: '#636e72' }}>{item.position}</td>
|
||||||
|
<td style={{ padding: '12px 20px', fontSize: '14px', color: '#6c5ce7', fontWeight: 500 }}>
|
||||||
|
📞 {item.phone}
|
||||||
|
</td>
|
||||||
|
</motion.tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{filteredData.length === 0 && (
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center', color: '#636e72' }}>
|
||||||
|
Ничего не найдено
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'map' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '60px 40px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
border: '2px dashed #dfe6e9',
|
||||||
|
minHeight: '300px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onClick={handleOpenMap}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#2ecc71';
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 30px rgba(46, 204, 113, 0.15)';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-3px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#dfe6e9';
|
||||||
|
e.currentTarget.style.boxShadow = '0 4px 20px rgba(0,0,0,0.06)';
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '64px', marginBottom: '15px' }}>🗺️</div>
|
||||||
|
<h3 style={{ color: '#2d3436', marginBottom: '10px', fontSize: '22px' }}>
|
||||||
|
Карта предприятия
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#636e72', marginBottom: '20px', fontSize: '15px' }}>
|
||||||
|
Нажмите для просмотра карты в полноэкранном режиме
|
||||||
|
</p>
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '12px 30px',
|
||||||
|
background: 'linear-gradient(135deg, #2ecc71, #27ae60)',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '10px',
|
||||||
|
fontSize: '15px',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}>
|
||||||
|
Открыть карту →
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Contacts;
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface FinanceProps {
|
||||||
|
onBack: () => void;
|
||||||
|
onViewPayslip: (period: string) => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinancialIndicator {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
change?: number;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Finance: React.FC<FinanceProps> = ({ onBack, onViewPayslip, employee }) => {
|
||||||
|
const [selectedPeriod, setSelectedPeriod] = useState(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
return `${month}${year}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [indicators] = useState<FinancialIndicator[]>([
|
||||||
|
{ title: 'Средняя ЗП по предприятию', value: '1 850', unit: 'руб.' },
|
||||||
|
{ title: 'Премия по итогам месяца', value: '15', unit: '%' },
|
||||||
|
{ title: 'Материальная помощь к отпуску', value: '500', unit: 'руб.' },
|
||||||
|
{ title: 'Выплаты к Новому году', value: '300', unit: 'руб.' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const generatePeriods = () => {
|
||||||
|
const periods = [];
|
||||||
|
const now = new Date();
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
periods.push(`${month}${year}`);
|
||||||
|
}
|
||||||
|
return periods;
|
||||||
|
};
|
||||||
|
|
||||||
|
const periods = generatePeriods();
|
||||||
|
const monthNames = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||||||
|
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
|
||||||
|
|
||||||
|
const formatPeriod = (period: string) => {
|
||||||
|
const month = parseInt(period.substring(0, 2)) - 1;
|
||||||
|
const year = period.substring(2);
|
||||||
|
return `${monthNames[month]} ${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="finance-section"
|
||||||
|
style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto', paddingBottom: '80px' }}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '25px', flexWrap: 'wrap', gap: '10px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{ padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '14px', transition: 'all 0.3s ease' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = '#5a6268'; e.currentTarget.style.transform = 'translateX(-2px)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = '#6c757d'; e.currentTarget.style.transform = 'translateX(0)'; }}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
<h2 style={{ margin: 0, color: '#2d3436', fontSize: '24px', fontWeight: 300, display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
💰 Финансы
|
||||||
|
</h2>
|
||||||
|
<div style={{ width: '100px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.1 }}
|
||||||
|
style={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', borderRadius: '16px', padding: '25px 30px', color: 'white', marginBottom: '30px', boxShadow: '0 10px 30px rgba(102, 126, 234, 0.3)' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '14px', opacity: 0.8, marginBottom: '5px' }}>{employee.organization}</div>
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 500 }}>{employee.famaly} {employee.ima} {employee.otch}</div>
|
||||||
|
<div style={{ fontSize: '14px', opacity: 0.9, marginTop: '5px' }}>Табельный номер: {employee.tnom}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' }}>
|
||||||
|
<div style={{ fontSize: '12px', opacity: 0.7 }}>Доступно</div>
|
||||||
|
<div style={{ fontSize: '28px', fontWeight: 600 }}>0.00</div>
|
||||||
|
<div style={{ fontSize: '12px', opacity: 0.7 }}>руб.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}
|
||||||
|
style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', marginBottom: '25px' }}>
|
||||||
|
<h3 style={{ margin: '0 0 20px 0', color: '#2d3436', fontSize: '18px', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
📊 Финансово-экономические показатели
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '15px' }}>
|
||||||
|
{indicators.map((item, index) => (
|
||||||
|
<motion.div key={index} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 * index }}
|
||||||
|
style={{ background: '#f8f9fa', borderRadius: '12px', padding: '16px 20px', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72', marginBottom: '8px' }}>{item.title}</div>
|
||||||
|
<div style={{ fontSize: '24px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{item.value}
|
||||||
|
{item.unit && <span style={{ fontSize: '14px', fontWeight: 400, color: '#636e72', marginLeft: '4px' }}>{item.unit}</span>}
|
||||||
|
</div>
|
||||||
|
{item.change !== undefined && (
|
||||||
|
<div style={{ fontSize: '12px', color: item.change >= 0 ? '#2ecc71' : '#e74c3c', marginTop: '4px' }}>
|
||||||
|
{item.change >= 0 ? '↑' : '↓'} {Math.abs(item.change)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}
|
||||||
|
style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)' }}>
|
||||||
|
<h3 style={{ margin: '0 0 20px 0', color: '#2d3436', fontSize: '18px', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
📄 Расчётный лист
|
||||||
|
</h3>
|
||||||
|
<div style={{ display: 'flex', gap: '15px', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||||
|
<label htmlFor="period-select" style={{ display: 'block', fontSize: '13px', color: '#636e72', marginBottom: '6px' }}>
|
||||||
|
📅 Выберите период:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="period-select"
|
||||||
|
value={selectedPeriod}
|
||||||
|
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||||
|
style={{ width: '100%', padding: '12px 16px', border: '2px solid #dfe6e9', borderRadius: '10px', fontSize: '15px', background: 'white', cursor: 'pointer', outline: 'none', transition: 'border-color 0.3s ease' }}
|
||||||
|
onFocus={(e) => e.target.style.borderColor = '#6c5ce7'}
|
||||||
|
onBlur={(e) => e.target.style.borderColor = '#dfe6e9'}
|
||||||
|
>
|
||||||
|
{periods.map(period => (
|
||||||
|
<option key={period} value={period}>{formatPeriod(period)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onViewPayslip(selectedPeriod)}
|
||||||
|
style={{ padding: '12px 30px', background: 'linear-gradient(135deg, #6c5ce7, #a29bfe)', color: 'white', border: 'none', borderRadius: '10px', fontSize: '15px', fontWeight: 500, cursor: 'pointer', transition: 'all 0.3s ease', boxShadow: '0 4px 15px rgba(108, 92, 231, 0.3)', display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 6px 20px rgba(108, 92, 231, 0.4)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = '0 4px 15px rgba(108, 92, 231, 0.3)'; }}
|
||||||
|
>
|
||||||
|
📄 Просмотреть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '20px', padding: '16px 20px', background: '#fff5f5', borderRadius: '10px', border: '1px solid #fde8e8' }}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72', marginBottom: '5px' }}>💡 Информация о премии</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#2d3436' }}>
|
||||||
|
Премия по итогам работы за {formatPeriod(selectedPeriod)} составляет 15% от оклада.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Finance;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import config from "../../src/config.json";
|
import { motion } from 'framer-motion';
|
||||||
|
import config from '../config.json';
|
||||||
|
|
||||||
interface GuestModeProps {
|
interface GuestModeProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
@@ -176,39 +178,136 @@ const GuestMode: React.FC<GuestModeProps> = ({ onBack }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="guest-mode">
|
<motion.div
|
||||||
<div className="top-bar">
|
className="guest-mode"
|
||||||
<button className="back-button" onClick={onBack}>
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
style={{
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '20px auto',
|
||||||
|
padding: '30px',
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.08)',
|
||||||
|
minHeight: '80vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ marginBottom: '20px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#5a6268';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#6c757d';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
← Назад
|
← Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="guest-content">
|
<div className="guest-content">
|
||||||
<h2>Гостевой режим</h2>
|
<h2 style={{
|
||||||
<div className="tab-navigation">
|
textAlign: 'center',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: '30px',
|
||||||
|
}}>
|
||||||
|
Гостевой режим
|
||||||
|
</h2>
|
||||||
|
<div className="tab-navigation" style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
marginBottom: '25px',
|
||||||
|
borderBottom: '2px solid #f0f0f0',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
}}>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'handbook' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'handbook' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('handbook')}
|
onClick={() => setActiveTab('handbook')}
|
||||||
|
style={{
|
||||||
|
padding: '12px 28px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: activeTab === 'handbook' ? 600 : 400,
|
||||||
|
color: activeTab === 'handbook' ? '#6c5ce7' : '#636e72',
|
||||||
|
borderBottom: activeTab === 'handbook' ? '3px solid #6c5ce7' : '3px solid transparent',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Справочник
|
Справочник
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'union' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'union' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('union')}
|
onClick={() => setActiveTab('union')}
|
||||||
|
style={{
|
||||||
|
padding: '12px 28px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: activeTab === 'union' ? 600 : 400,
|
||||||
|
color: activeTab === 'union' ? '#6c5ce7' : '#636e72',
|
||||||
|
borderBottom: activeTab === 'union' ? '3px solid #6c5ce7' : '3px solid transparent',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Профсоюз
|
Профсоюз
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('info')}
|
onClick={() => setActiveTab('info')}
|
||||||
|
style={{
|
||||||
|
padding: '12px 28px',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: activeTab === 'info' ? 600 : 400,
|
||||||
|
color: activeTab === 'info' ? '#6c5ce7' : '#636e72',
|
||||||
|
borderBottom: activeTab === 'info' ? '3px solid #6c5ce7' : '3px solid transparent',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Общая информация
|
Общая информация
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="tab-content">
|
<div className="tab-content" style={{ minHeight: '400px' }}>
|
||||||
{activeTab === 'handbook' && (
|
{activeTab === 'handbook' && (
|
||||||
<div className="handbook-content">
|
<div className="handbook-content">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Загрузка справочника...</div>
|
<div className="loading" style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#636e72',
|
||||||
|
padding: '40px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '3px solid #f3f3f3',
|
||||||
|
borderTop: '3px solid #6c5ce7',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}} />
|
||||||
|
<p>Загрузка справочника...</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<HandbookBlocks html={handbookContent} />
|
<HandbookBlocks html={handbookContent} />
|
||||||
)}
|
)}
|
||||||
@@ -217,7 +316,22 @@ const GuestMode: React.FC<GuestModeProps> = ({ onBack }) => {
|
|||||||
{activeTab === 'union' && (
|
{activeTab === 'union' && (
|
||||||
<div className="union-content">
|
<div className="union-content">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Загрузка...</div>
|
<div className="loading" style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#636e72',
|
||||||
|
padding: '40px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '3px solid #f3f3f3',
|
||||||
|
borderTop: '3px solid #6c5ce7',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}} />
|
||||||
|
<p>Загрузка...</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
unionBlocks.length ? renderContentBlocks(unionBlocks) : <p>Нет информации о профсоюзе.</p>
|
unionBlocks.length ? renderContentBlocks(unionBlocks) : <p>Нет информации о профсоюзе.</p>
|
||||||
)}
|
)}
|
||||||
@@ -226,7 +340,22 @@ const GuestMode: React.FC<GuestModeProps> = ({ onBack }) => {
|
|||||||
{activeTab === 'info' && (
|
{activeTab === 'info' && (
|
||||||
<div className="info-content">
|
<div className="info-content">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Загрузка...</div>
|
<div className="loading" style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#636e72',
|
||||||
|
padding: '40px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '3px solid #f3f3f3',
|
||||||
|
borderTop: '3px solid #6c5ce7',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}} />
|
||||||
|
<p>Загрузка...</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
infoBlocks.length ? renderContentBlocks(infoBlocks) : <p>Нет общей информации.</p>
|
infoBlocks.length ? renderContentBlocks(infoBlocks) : <p>Нет общей информации.</p>
|
||||||
)}
|
)}
|
||||||
@@ -234,7 +363,15 @@ const GuestMode: React.FC<GuestModeProps> = ({ onBack }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface LaborSafetyProps {
|
||||||
|
onBack: () => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LaborSafety: React.FC<LaborSafetyProps> = ({ onBack, employee }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="labor-safety-section"
|
||||||
|
style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto', paddingBottom: '80px' }}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ marginBottom: '25px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{ padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '14px', transition: 'all 0.3s ease' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = '#5a6268'; e.currentTarget.style.transform = 'translateX(-2px)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = '#6c757d'; e.currentTarget.style.transform = 'translateX(0)'; }}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{ color: '#2d3436', fontSize: '24px', fontWeight: 300, marginBottom: '30px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
⛑️ Охрана труда
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '20px' }}>
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#e74c3c', marginBottom: '15px' }}>⚠️</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Изменения в документации</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Информирование о внесённых изменениях в ОЗИ и СТП</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#3498db', marginBottom: '15px' }}>📋</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Средства индивидуальной защиты (СИЗ)</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>График выдачи, напоминания, информация по СИЗ</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#9b59b6', marginBottom: '15px' }}>📅</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>График экзаменов</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Просмотр графика сдачи экзаменов по охране труда</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#2ecc71', marginBottom: '15px' }}>🔔</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Уведомления</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Напоминания о своевременном получении СИЗ</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '30px', padding: '20px', background: '#f8f9fa', borderRadius: '12px', border: '1px solid #f1f2f6', textAlign: 'center', color: '#636e72' }}>
|
||||||
|
<p style={{ margin: 0 }}>ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время.</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LaborSafety;
|
||||||
@@ -1,49 +1,88 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
interface LoginWindowProps {
|
interface LoginWindowProps {
|
||||||
onLogin: (id: string) => void;
|
onLogin: (id: string) => void;
|
||||||
onGuestMode: () => void;
|
onGuestMode: () => void;
|
||||||
loginError?: string;
|
loginError?: string;
|
||||||
|
inputRef?: React.RefObject<HTMLInputElement>; // ✅ Реф для управления фокусом
|
||||||
}
|
}
|
||||||
const LoginWindow: React.FC<LoginWindowProps> = ({ onLogin, onGuestMode, loginError }) => {
|
|
||||||
|
const LoginWindow: React.FC<LoginWindowProps> = ({
|
||||||
|
onLogin,
|
||||||
|
onGuestMode,
|
||||||
|
loginError,
|
||||||
|
inputRef
|
||||||
|
}) => {
|
||||||
const [id, setId] = useState('');
|
const [id, setId] = useState('');
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const [isCardDetected, setIsCardDetected] = useState(false);
|
||||||
|
|
||||||
|
// ✅ Используем переданный реф или внутренний
|
||||||
|
const internalInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const activeInputRef = inputRef || internalInputRef;
|
||||||
|
|
||||||
|
// ✅ При монтировании — фокус на поле ввода
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeInputRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
activeInputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [activeInputRef]);
|
||||||
|
|
||||||
|
// ✅ При ошибке — фокус на поле ввода
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginError) {
|
||||||
|
setId('');
|
||||||
|
if (activeInputRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
activeInputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loginError, activeInputRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://localhost:3001/api/test', {
|
const response = await fetch('http://localhost:3001/api/test', {
|
||||||
mode: 'cors', // Явно указываем режим CORS
|
mode: 'cors',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Backend response:', data);
|
console.log('Backend response:', data);
|
||||||
} else {
|
} else {
|
||||||
console.error('Backend returned error status:', response.status);
|
console.error('Backend returned error status:', response.status);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Connection test failed:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
console.error('Connection test failed:', err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
testConnection();
|
testConnection();
|
||||||
|
|
||||||
const setupCardListener = () => {
|
const setupCardListener = () => {
|
||||||
console.log('[LoginWindow] Setting up card listener...');
|
console.log('[LoginWindow] Setting up card listener...');
|
||||||
const handleCardDetected = (uid: string) => {
|
const handleCardDetected = (uid: string) => {
|
||||||
console.log('Card detected in renderer via preload:', uid);
|
console.log('Card detected in renderer via preload:', uid);
|
||||||
|
setIsCardDetected(true);
|
||||||
onLogin(uid);
|
onLogin(uid);
|
||||||
|
setTimeout(() => setIsCardDetected(false), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.electronAPI.onCardDetected(handleCardDetected);
|
if (window.electronAPI) {
|
||||||
|
window.electronAPI.onCardDetected(handleCardDetected);
|
||||||
|
} else {
|
||||||
|
console.log('[LoginWindow] electronAPI not ready, waiting for event...');
|
||||||
|
window.addEventListener('electronApiReady', setupCardListener, { once: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (window.electronAPI) {
|
|
||||||
setupCardListener();
|
setupCardListener();
|
||||||
} else {
|
|
||||||
console.log('[LoginWindow] electronAPI not ready, waiting for event...');
|
|
||||||
window.addEventListener('electronApiReady', setupCardListener, { once: true });
|
|
||||||
}
|
|
||||||
return () => {
|
return () => {
|
||||||
console.log('[LoginWindow] Cleaning up listeners.');
|
console.log('[LoginWindow] Cleaning up listeners.');
|
||||||
window.removeEventListener('electronApiReady', setupCardListener);
|
window.removeEventListener('electronApiReady', setupCardListener);
|
||||||
@@ -53,13 +92,6 @@ const LoginWindow: React.FC<LoginWindowProps> = ({ onLogin, onGuestMode, loginEr
|
|||||||
};
|
};
|
||||||
}, [onLogin]);
|
}, [onLogin]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loginError) {
|
|
||||||
setId('');
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, [loginError]);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (id.trim()) {
|
if (id.trim()) {
|
||||||
@@ -68,37 +100,216 @@ const LoginWindow: React.FC<LoginWindowProps> = ({ onLogin, onGuestMode, loginEr
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-window">
|
<motion.div
|
||||||
<h2>Вход в систему</h2>
|
className="login-window"
|
||||||
<div className="card-prompt">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<p>Пожалуйста, приложите вашу карту пропуска к считывателю</p>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<div className="card-icon">💳</div>
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
style={{
|
||||||
|
maxWidth: '420px',
|
||||||
|
margin: '10% auto',
|
||||||
|
padding: '40px',
|
||||||
|
background: 'rgba(255, 255, 255, 0.98)',
|
||||||
|
borderRadius: '20px',
|
||||||
|
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.15)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Индикатор считывания карты */}
|
||||||
|
{isCardDetected && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-10px',
|
||||||
|
right: '-10px',
|
||||||
|
background: '#2ecc71',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '6px 12px',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
boxShadow: '0 4px 15px rgba(46, 204, 113, 0.4)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✅ Карта считана
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: '10px',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
}}>
|
||||||
|
Инфокиоск
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#636e72',
|
||||||
|
fontSize: '14px',
|
||||||
|
marginBottom: '30px',
|
||||||
|
opacity: 0.7,
|
||||||
|
}}>
|
||||||
|
Приложите карту пропуска
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '64px',
|
||||||
|
animation: 'pulse 2s ease-in-out infinite',
|
||||||
|
filter: isCardDetected ? 'drop-shadow(0 0 20px rgba(46, 204, 113, 0.5))' : 'none',
|
||||||
|
}}>
|
||||||
|
💳
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="input-group">
|
<div className="input-group" style={{ marginBottom: '15px' }}>
|
||||||
<label htmlFor="id"></label>
|
<label htmlFor="id" style={{
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: '#636e72',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.5px',
|
||||||
|
}}>
|
||||||
|
Введите ID или приложите карту
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={activeInputRef}
|
||||||
type="password"
|
type="text"
|
||||||
id="id"
|
id="id"
|
||||||
value={id}
|
value={id}
|
||||||
onChange={(e) => setId(e.target.value)}
|
onChange={(e) => setId(e.target.value)}
|
||||||
placeholder=" "
|
placeholder="Введите табельный номер..."
|
||||||
autoFocus
|
autoFocus
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '14px 16px',
|
||||||
|
border: `2px solid ${loginError ? '#e74c3c' : '#dfe6e9'}`,
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '16px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
outline: 'none',
|
||||||
|
}}
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.style.borderColor = '#6c5ce7';
|
||||||
|
e.target.style.boxShadow = '0 0 0 4px rgba(108, 92, 231, 0.1)';
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
e.target.style.borderColor = loginError ? '#e74c3c' : '#dfe6e9';
|
||||||
|
e.target.style.boxShadow = 'none';
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{loginError && <div className="error" style={{ color: 'red', marginTop: 8 }}>{loginError}</div>}
|
|
||||||
<div className="button-group">
|
{loginError && (
|
||||||
<button type="submit" disabled={!id.trim()}>
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
style={{
|
||||||
|
color: '#e74c3c',
|
||||||
|
fontSize: '13px',
|
||||||
|
marginTop: '-10px',
|
||||||
|
marginBottom: '15px',
|
||||||
|
background: '#fde8e8',
|
||||||
|
padding: '10px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #f5c6cb',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚠️ {loginError}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '12px',
|
||||||
|
marginTop: '20px',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!id.trim()}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '14px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: id.trim() ? 'pointer' : 'not-allowed',
|
||||||
|
background: id.trim() ? 'linear-gradient(135deg, #6c5ce7, #a29bfe)' : '#dfe6e9',
|
||||||
|
color: id.trim() ? '#fff' : '#b2bec3',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
boxShadow: id.trim() ? '0 4px 15px rgba(108, 92, 231, 0.3)' : 'none',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (id.trim()) {
|
||||||
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
Войти
|
Войти
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onGuestMode}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onGuestMode}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '14px',
|
||||||
|
border: '2px solid #dfe6e9',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#636e72',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#6c5ce7';
|
||||||
|
e.currentTarget.style.color = '#6c5ce7';
|
||||||
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.borderColor = '#dfe6e9';
|
||||||
|
e.currentTarget.style.color = '#636e72';
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
Гостевой режим
|
Гостевой режим
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
<div style={{
|
||||||
|
marginTop: '20px',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#b2bec3',
|
||||||
|
borderTop: '1px solid #f1f2f6',
|
||||||
|
paddingTop: '15px',
|
||||||
|
}}>
|
||||||
|
<span>Для автоматического входа приложите карту к считывателю</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,863 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
FaArrowLeft,
|
||||||
|
FaSpinner,
|
||||||
|
FaPlus,
|
||||||
|
FaMinus,
|
||||||
|
FaHome,
|
||||||
|
FaArrowUp,
|
||||||
|
FaArrowDown,
|
||||||
|
FaArrowLeft as FaArrowLeftIcon,
|
||||||
|
FaArrowRight,
|
||||||
|
FaSearch,
|
||||||
|
FaTimes,
|
||||||
|
FaMapMarkerAlt
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface MapViewerProps {
|
||||||
|
fileUrl: string;
|
||||||
|
onBack: () => void;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapMarker {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MapViewer: React.FC<MapViewerProps> = ({
|
||||||
|
fileUrl,
|
||||||
|
onBack,
|
||||||
|
title = 'Карта предприятия'
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [minZoom, setMinZoom] = useState(1);
|
||||||
|
const [showControls, setShowControls] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
const [searchResults, setSearchResults] = useState<MapMarker[]>([]);
|
||||||
|
|
||||||
|
// 📐 Реальные размеры изображения
|
||||||
|
const [imgWidth, setImgWidth] = useState(0);
|
||||||
|
const [imgHeight, setImgHeight] = useState(0);
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
|
||||||
|
// 📍 Панорамирование
|
||||||
|
const [offsetX, setOffsetX] = useState(0);
|
||||||
|
const [offsetY, setOffsetY] = useState(0);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dragStartX, setDragStartX] = useState(0);
|
||||||
|
const [dragStartY, setDragStartY] = useState(0);
|
||||||
|
const [startOffsetX, setStartOffsetX] = useState(0);
|
||||||
|
const [startOffsetY, setStartOffsetY] = useState(0);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
// 📍 Тестовые метки
|
||||||
|
const markers: MapMarker[] = [
|
||||||
|
{ id: '1', name: 'Главный вход', x: 50, y: 20, description: 'Вход для посетителей' },
|
||||||
|
{ id: '2', name: 'Проходная', x: 30, y: 50, description: 'КПП №1' },
|
||||||
|
{ id: '3', name: 'Склад', x: 70, y: 70, description: 'Склад готовой продукции' },
|
||||||
|
{ id: '4', name: 'Столовая', x: 45, y: 80, description: 'Столовая №2' },
|
||||||
|
{ id: '5', name: 'Администрация', x: 20, y: 30, description: 'Главный офис' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 📄 Загрузка изображения
|
||||||
|
useEffect(() => {
|
||||||
|
const loadImage = () => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
setImgWidth(img.width);
|
||||||
|
setImgHeight(img.height);
|
||||||
|
setImageLoaded(true);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
img.onerror = () => {
|
||||||
|
setError('Не удалось загрузить изображение');
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
img.src = fileUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
loadImage();
|
||||||
|
}, [fileUrl]);
|
||||||
|
|
||||||
|
// ✅ Расчет минимального масштаба ПОСЛЕ загрузки изображения и рендера контейнера
|
||||||
|
useEffect(() => {
|
||||||
|
if (!imageLoaded || !containerRef.current) return;
|
||||||
|
|
||||||
|
const calculateMinZoom = () => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || imgHeight === 0) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const newMinZoom = rect.height / imgHeight;
|
||||||
|
setMinZoom(newMinZoom);
|
||||||
|
setZoom(newMinZoom);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ждем один кадр, чтобы DOM обновился
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
calculateMinZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Также пересчитываем при ресайзе
|
||||||
|
const handleResize = () => {
|
||||||
|
if (imgHeight > 0 && containerRef.current) {
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
const newMinZoom = rect.height / imgHeight;
|
||||||
|
setMinZoom(newMinZoom);
|
||||||
|
setZoom(prev => Math.max(prev, newMinZoom));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [imageLoaded, imgHeight]);
|
||||||
|
|
||||||
|
// 🔄 Масштабирование
|
||||||
|
const handleZoomIn = () => {
|
||||||
|
setZoom(prev => Math.min(prev + 0.2, 3));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomOut = () => {
|
||||||
|
setZoom(prev => Math.max(prev - 0.2, minZoom));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleZoomReset = () => {
|
||||||
|
setZoom(minZoom);
|
||||||
|
setOffsetX(0);
|
||||||
|
setOffsetY(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 📍 Получение границ
|
||||||
|
const getBounds = useCallback(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container || imgWidth === 0 || imgHeight === 0) {
|
||||||
|
return { maxX: 0, maxY: 0 };
|
||||||
|
}
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const displayWidth = imgWidth * zoom;
|
||||||
|
const displayHeight = imgHeight * zoom;
|
||||||
|
const maxX = Math.max(0, (displayWidth - rect.width) / 2);
|
||||||
|
const maxY = Math.max(0, (displayHeight - rect.height) / 2);
|
||||||
|
return { maxX, maxY };
|
||||||
|
}, [imgWidth, imgHeight, zoom]);
|
||||||
|
|
||||||
|
// 📍 Ограничение смещения
|
||||||
|
const clampOffset = useCallback((x: number, y: number) => {
|
||||||
|
const { maxX, maxY } = getBounds();
|
||||||
|
return {
|
||||||
|
x: Math.max(-maxX, Math.min(maxX, x)),
|
||||||
|
y: Math.max(-maxY, Math.min(maxY, y))
|
||||||
|
};
|
||||||
|
}, [getBounds]);
|
||||||
|
|
||||||
|
// 📍 Панорамирование
|
||||||
|
const handlePan = useCallback((dx: number, dy: number) => {
|
||||||
|
const step = 80 / zoom;
|
||||||
|
const newX = offsetX + dx * step;
|
||||||
|
const newY = offsetY + dy * step;
|
||||||
|
const clamped = clampOffset(newX, newY);
|
||||||
|
setOffsetX(clamped.x);
|
||||||
|
setOffsetY(clamped.y);
|
||||||
|
}, [offsetX, offsetY, zoom, clampOffset]);
|
||||||
|
|
||||||
|
// 🖱️ Drag
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragStartX(e.clientX);
|
||||||
|
setDragStartY(e.clientY);
|
||||||
|
setStartOffsetX(offsetX);
|
||||||
|
setStartOffsetY(offsetY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const dx = e.clientX - dragStartX;
|
||||||
|
const dy = e.clientY - dragStartY;
|
||||||
|
const newX = startOffsetX + dx;
|
||||||
|
const newY = startOffsetY + dy;
|
||||||
|
const clamped = clampOffset(newX, newY);
|
||||||
|
setOffsetX(clamped.x);
|
||||||
|
setOffsetY(clamped.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragStartX(touch.clientX);
|
||||||
|
setDragStartY(touch.clientY);
|
||||||
|
setStartOffsetX(offsetX);
|
||||||
|
setStartOffsetY(offsetY);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: React.TouchEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const dx = touch.clientX - dragStartX;
|
||||||
|
const dy = touch.clientY - dragStartY;
|
||||||
|
const newX = startOffsetX + dx;
|
||||||
|
const newY = startOffsetY + dy;
|
||||||
|
const clamped = clampOffset(newX, newY);
|
||||||
|
setOffsetX(clamped.x);
|
||||||
|
setOffsetY(clamped.y);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔍 Поиск
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
setSearchQuery(query);
|
||||||
|
if (query.trim().length > 0) {
|
||||||
|
const results = markers.filter(marker =>
|
||||||
|
marker.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
(marker.description && marker.description.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
);
|
||||||
|
setSearchResults(results);
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
const marker = results[0];
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (container) {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const displayWidth = imgWidth * zoom;
|
||||||
|
const displayHeight = imgHeight * zoom;
|
||||||
|
const targetX = (marker.x / 100) * displayWidth - rect.width / 2;
|
||||||
|
const targetY = (marker.y / 100) * displayHeight - rect.height / 2;
|
||||||
|
const clamped = clampOffset(-targetX, -targetY);
|
||||||
|
setOffsetX(clamped.x);
|
||||||
|
setOffsetY(clamped.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSearchResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
setShowSearch(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⏱️ Авто-скрытие
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout;
|
||||||
|
const handleActivity = () => {
|
||||||
|
setShowControls(true);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setShowControls(false);
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousemove', handleActivity);
|
||||||
|
document.addEventListener('touchstart', handleActivity);
|
||||||
|
document.addEventListener('keydown', handleActivity);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleActivity);
|
||||||
|
document.removeEventListener('touchstart', handleActivity);
|
||||||
|
document.removeEventListener('keydown', handleActivity);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ⌨️ ESC
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (showSearch) {
|
||||||
|
clearSearch();
|
||||||
|
} else {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
return () => document.removeEventListener('keydown', handleEscape);
|
||||||
|
}, [showSearch, onBack]);
|
||||||
|
|
||||||
|
// 🎨 Рендер
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSpinner style={{
|
||||||
|
fontSize: '60px',
|
||||||
|
color: '#2ecc71',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}} />
|
||||||
|
<span style={{ color: 'white', marginTop: '20px', fontSize: '18px' }}>
|
||||||
|
Загрузка карты...
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: 'rgba(0, 0, 0, 0.9)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10000,
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '64px', marginBottom: '20px' }}>🗺️</div>
|
||||||
|
<h3 style={{ color: 'white', margin: '0 0 10px 0', fontSize: '24px' }}>
|
||||||
|
Карта не найдена
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#b2bec3', margin: '0 0 10px 0', textAlign: 'center' }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '16px 40px',
|
||||||
|
background: '#2ecc71',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Вернуться
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: '#1a1a2e',
|
||||||
|
zIndex: 10000,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ⬅️ Кнопка Назад */}
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '16px',
|
||||||
|
left: '16px',
|
||||||
|
zIndex: 100,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '10px 18px',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.15)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowLeft style={{ fontSize: '16px' }} />
|
||||||
|
<span>Назад</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 🔍 Поле поиска — сверху справа */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '16px',
|
||||||
|
right: '16px',
|
||||||
|
zIndex: 100,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
background: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
borderRadius: '24px',
|
||||||
|
padding: '6px 12px 6px 16px',
|
||||||
|
border: showSearch ? '2px solid #4285f4' : '2px solid transparent',
|
||||||
|
boxShadow: showSearch ? '0 4px 20px rgba(66, 133, 244, 0.3)' : '0 2px 10px rgba(0,0,0,0.2)',
|
||||||
|
minWidth: showSearch ? '280px' : '44px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
height: '44px',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSearch(!showSearch)}
|
||||||
|
style={{
|
||||||
|
padding: '4px',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#5f6368',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minWidth: '28px',
|
||||||
|
minHeight: '28px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaSearch />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showSearch && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0, opacity: 0 }}
|
||||||
|
animate={{ width: 'auto', opacity: 1 }}
|
||||||
|
exit={{ width: 0, opacity: 0 }}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
placeholder="Поиск по карте..."
|
||||||
|
autoFocus
|
||||||
|
style={{
|
||||||
|
padding: '8px 0',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#202124',
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
fontSize: '15px',
|
||||||
|
minWidth: '180px',
|
||||||
|
width: '100%',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={clearSearch}
|
||||||
|
style={{
|
||||||
|
padding: '4px 6px',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#5f6368',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '16px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaTimes />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 📍 Результаты поиска */}
|
||||||
|
{searchResults.length > 0 && showSearch && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '68px',
|
||||||
|
right: '16px',
|
||||||
|
zIndex: 99,
|
||||||
|
background: 'rgba(255, 255, 255, 0.98)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(0,0,0,0.1)',
|
||||||
|
boxShadow: '0 8px 30px rgba(0,0,0,0.2)',
|
||||||
|
padding: '8px 0',
|
||||||
|
minWidth: '300px',
|
||||||
|
maxHeight: '300px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}>
|
||||||
|
{searchResults.map((marker) => (
|
||||||
|
<button
|
||||||
|
key={marker.id}
|
||||||
|
onClick={() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (container) {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const displayWidth = imgWidth * zoom;
|
||||||
|
const displayHeight = imgHeight * zoom;
|
||||||
|
const targetX = (marker.x / 100) * displayWidth - rect.width / 2;
|
||||||
|
const targetY = (marker.y / 100) * displayHeight - rect.height / 2;
|
||||||
|
const clamped = clampOffset(-targetX, -targetY);
|
||||||
|
setOffsetX(clamped.x);
|
||||||
|
setOffsetY(clamped.y);
|
||||||
|
}
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
setShowSearch(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '12px 20px',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#202124',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'left',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f1f3f4';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaMapMarkerAlt style={{ color: '#ea4335', fontSize: '18px', flexShrink: 0 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{marker.name}</div>
|
||||||
|
{marker.description && (
|
||||||
|
<div style={{ fontSize: '12px', color: '#5f6368' }}>
|
||||||
|
{marker.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 🗺️ Контейнер с картой */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: '#1a1a2e',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab',
|
||||||
|
touchAction: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseLeave={handleMouseUp}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
{/* ✅ TIFF изображение */}
|
||||||
|
<img
|
||||||
|
ref={imageRef}
|
||||||
|
src={fileUrl}
|
||||||
|
alt="Карта предприятия"
|
||||||
|
style={{
|
||||||
|
width: `${imgWidth * zoom}px`,
|
||||||
|
height: `${imgHeight * zoom}px`,
|
||||||
|
transform: `translate(${offsetX}px, ${offsetY}px)`,
|
||||||
|
transformOrigin: 'center center',
|
||||||
|
transition: isDragging ? 'none' : 'transform 0.3s ease',
|
||||||
|
willChange: 'transform',
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
marginLeft: `${-imgWidth * zoom / 2}px`,
|
||||||
|
marginTop: `${-imgHeight * zoom / 2}px`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
objectFit: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 👆 Подсказка */}
|
||||||
|
{!isDragging && showControls && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '110px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
color: 'rgba(255,255,255,0.5)',
|
||||||
|
fontSize: '13px',
|
||||||
|
textAlign: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 50,
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
padding: '8px 20px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
backdropFilter: 'blur(5px)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
👆 Перетащите для перемещения
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 🎮 Нижняя панель управления */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showControls && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: 100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 100, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
background: 'linear-gradient(0deg, rgba(0,0,0,0.9) 0%, transparent 100%)',
|
||||||
|
padding: '30px 16px 20px 16px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '16px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Кнопки панорамирования */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '56px 56px 56px',
|
||||||
|
gridTemplateRows: '56px 56px 56px',
|
||||||
|
gap: '5px',
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePan(0, 1)}
|
||||||
|
style={{
|
||||||
|
gridColumn: '2',
|
||||||
|
gridRow: '1',
|
||||||
|
padding: '4px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowUp />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePan(1, 0)}
|
||||||
|
style={{
|
||||||
|
gridColumn: '1',
|
||||||
|
gridRow: '2',
|
||||||
|
padding: '4px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowLeftIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleZoomReset}
|
||||||
|
style={{
|
||||||
|
gridColumn: '2',
|
||||||
|
gridRow: '2',
|
||||||
|
padding: '4px',
|
||||||
|
background: 'rgba(46, 204, 113, 0.25)',
|
||||||
|
color: '#2ecc71',
|
||||||
|
border: '1px solid rgba(46, 204, 113, 0.15)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaHome />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePan(-1, 0)}
|
||||||
|
style={{
|
||||||
|
gridColumn: '3',
|
||||||
|
gridRow: '2',
|
||||||
|
padding: '4px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowRight />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePan(0, -1)}
|
||||||
|
style={{
|
||||||
|
gridColumn: '2',
|
||||||
|
gridRow: '3',
|
||||||
|
padding: '4px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaArrowDown />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопки масштабирования */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '6px',
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
disabled={zoom <= minZoom}
|
||||||
|
style={{
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: zoom > minZoom ? 'pointer' : 'default',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 600,
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
minWidth: '48px',
|
||||||
|
minHeight: '48px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
opacity: zoom > minZoom ? 1 : 0.3,
|
||||||
|
pointerEvents: zoom > minZoom ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaMinus />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span style={{
|
||||||
|
color: 'rgba(255,255,255,0.8)',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: 600,
|
||||||
|
minWidth: '44px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
{Math.round(zoom * 100)}%
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
style={{
|
||||||
|
padding: '12px 16px',
|
||||||
|
background: 'rgba(255,255,255,0.10)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.06)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 600,
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
minWidth: '48px',
|
||||||
|
minHeight: '48px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MapViewer;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface MedicineProps {
|
||||||
|
onBack: () => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Medicine: React.FC<MedicineProps> = ({ onBack, employee }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="medicine-section"
|
||||||
|
style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto', paddingBottom: '80px' }}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ marginBottom: '25px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{ padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '14px', transition: 'all 0.3s ease' }}
|
||||||
|
onMouseEnter={(e) => { e.currentTarget.style.background = '#5a6268'; e.currentTarget.style.transform = 'translateX(-2px)'; }}
|
||||||
|
onMouseLeave={(e) => { e.currentTarget.style.background = '#6c757d'; e.currentTarget.style.transform = 'translateX(0)'; }}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{ color: '#2d3436', fontSize: '24px', fontWeight: 300, marginBottom: '30px', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
🏥 Медицина
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '20px' }}>
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#3498db', marginBottom: '15px' }}>📋</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Медицинская карта</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Просмотр информации из медицинской карты</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#2ecc71', marginBottom: '15px' }}>🏥</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Запись в поликлинику</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Онлайн запись к врачу</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ y: -5 }} style={{ background: 'white', borderRadius: '16px', padding: '25px', boxShadow: '0 4px 20px rgba(0,0,0,0.06)', cursor: 'pointer', transition: 'all 0.3s ease', border: '1px solid #f1f2f6' }}>
|
||||||
|
<div style={{ fontSize: '32px', color: '#f39c12', marginBottom: '15px' }}>🛡️</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>Страховая компания</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>Оформление заявок на медицинские услуги</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '30px', padding: '20px', background: '#f8f9fa', borderRadius: '12px', border: '1px solid #f1f2f6', textAlign: 'center', color: '#636e72' }}>
|
||||||
|
<p style={{ margin: 0 }}>ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время.</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Medicine;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface MainMenuProps {
|
||||||
|
onSectionSelect: (section: string) => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MainMenu: React.FC<MainMenuProps> = ({ onSectionSelect, employee }) => {
|
||||||
|
console.log('🔵 MainMenu компонент рендерится!');
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ id: 'employee-info', title: 'Моя информация', emoji: '📋' },
|
||||||
|
{ id: 'finance', title: 'Финансы', emoji: '💰' },
|
||||||
|
{ id: 'medicine', title: 'Медицина', emoji: '🏥' },
|
||||||
|
{ id: 'trade-union', title: 'Профсоюз', emoji: '🤝' },
|
||||||
|
{ id: 'labor-safety', title: 'Охрана труда', emoji: '⛑️' },
|
||||||
|
{ id: 'contacts', title: 'Контакты', emoji: '📞' },
|
||||||
|
{ id: 'news', title: 'Новости', emoji: '📰' },
|
||||||
|
{ id: 'profile', title: 'Обо мне', emoji: '👤' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '40px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
}}>
|
||||||
|
{employee && (
|
||||||
|
<div style={{
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginBottom: '10px',
|
||||||
|
}}>
|
||||||
|
Здравствуйте, {employee.ima} {employee.famaly}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h1 style={{
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
marginBottom: '40px',
|
||||||
|
fontWeight: 300,
|
||||||
|
}}>
|
||||||
|
Выберите раздел
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 200px))',
|
||||||
|
gap: '25px',
|
||||||
|
maxWidth: '900px',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{menuItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => onSectionSelect(item.id)}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '25px 20px',
|
||||||
|
background: 'rgba(255,255,255,0.95)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '0 8px 30px rgba(0,0,0,0.12)',
|
||||||
|
minHeight: '140px',
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'scale(1.05)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 12px 40px rgba(0,0,0,0.2)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = 'scale(1)';
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 30px rgba(0,0,0,0.12)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{item.emoji}</div>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
color: '#2d3436',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🟢 MainMenu.tsx экспорт готов');
|
||||||
|
|
||||||
|
export default MainMenu;
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import React, { useState, type ComponentType, type SVGAttributes } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { FaNewspaper, FaCalendarAlt, FaUser, FaTag } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface NewsProps {
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewsItem {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
content: string;
|
||||||
|
category: string;
|
||||||
|
author: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconWrapper = ({ icon: Icon, style }: { icon: any; style?: React.CSSProperties }) => (
|
||||||
|
<Icon style={style} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const News: React.FC<NewsProps> = ({ onBack }) => {
|
||||||
|
const [news, setNews] = useState<NewsItem[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'День химика - поздравления коллективу!',
|
||||||
|
date: '2024-05-27',
|
||||||
|
content: 'Уважаемые сотрудники! Поздравляем вас с профессиональным праздником - Днем химика! Желаем успехов в работе, крепкого здоровья и благополучия.',
|
||||||
|
category: 'Праздники',
|
||||||
|
author: 'Администрация',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Новые правила охраны труда',
|
||||||
|
date: '2024-05-25',
|
||||||
|
content: 'С 1 июня вводятся новые правила по охране труда. Просим ознакомиться с изменениями в разделе "Охрана труда".',
|
||||||
|
category: 'Охрана труда',
|
||||||
|
author: 'Отдел ОТ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Спортивные соревнования среди цехов',
|
||||||
|
date: '2024-05-20',
|
||||||
|
content: 'Приглашаем принять участие в ежегодных спортивных соревнованиях. Заявки принимаются до 10 июня.',
|
||||||
|
category: 'Спорт',
|
||||||
|
author: 'Профсоюз',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('Все');
|
||||||
|
const categories = ['Все', ...Array.from(new Set(news.map(item => item.category)))];
|
||||||
|
|
||||||
|
const filteredNews = selectedCategory === 'Все'
|
||||||
|
? news
|
||||||
|
: news.filter(item => item.category === selectedCategory);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="news-section"
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
paddingBottom: '80px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ marginBottom: '25px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#5a6268';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#6c757d';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: '30px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}>
|
||||||
|
<IconWrapper icon={FaNewspaper} style={{ color: '#1abc9c' }} />
|
||||||
|
Новости
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Фильтр по категориям */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: '25px',
|
||||||
|
}}>
|
||||||
|
{categories.map(category => (
|
||||||
|
<motion.button
|
||||||
|
key={category}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
style={{
|
||||||
|
padding: '8px 20px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: selectedCategory === category ? 600 : 400,
|
||||||
|
background: selectedCategory === category ? '#1abc9c' : '#f8f9fa',
|
||||||
|
color: selectedCategory === category ? 'white' : '#636e72',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Список новостей */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '20px',
|
||||||
|
}}>
|
||||||
|
{filteredNews.map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
whileHover={{ y: -4 }}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '25px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
border: '1px solid #f1f2f6',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '10px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
}}>
|
||||||
|
<h3 style={{
|
||||||
|
margin: 0,
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}>
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<span style={{
|
||||||
|
padding: '4px 12px',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#636e72',
|
||||||
|
}}>
|
||||||
|
<IconWrapper icon={FaTag} style={{ marginRight: '4px', fontSize: '10px' }} />
|
||||||
|
{item.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{
|
||||||
|
color: '#636e72',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: 1.8,
|
||||||
|
margin: '0 0 15px 0',
|
||||||
|
}}>
|
||||||
|
{item.content}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: '#b2bec3',
|
||||||
|
}}>
|
||||||
|
<span>
|
||||||
|
<IconWrapper icon={FaCalendarAlt} style={{ marginRight: '5px' }} />
|
||||||
|
{new Date(item.date).toLocaleDateString('ru-RU')}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<IconWrapper icon={FaUser} style={{ marginRight: '5px' }} />
|
||||||
|
{item.author}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredNews.length === 0 && (
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '40px',
|
||||||
|
color: '#636e72',
|
||||||
|
}}>
|
||||||
|
Новостей в этой категории пока нет
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default News;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
// ИСПОЛЬЗОВАТЬ CDN - ЭТО РАБОТАЕТ
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
|
|
||||||
interface PayslipViewerProps {
|
interface PayslipViewerProps {
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
@@ -18,28 +20,28 @@ const PayslipViewer: React.FC<PayslipViewerProps> = ({ fileUrl, onBack }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="payslip-viewer">
|
<div className="payslip-viewer">
|
||||||
<div className="top-bar">
|
<div className="top-bar">
|
||||||
<button className="back-button" onClick={onBack}>
|
<button className="back-button" onClick={onBack}>
|
||||||
← Назад
|
← Назад
|
||||||
</button>
|
</button>
|
||||||
<button className="print-button" onClick={handlePrint}>
|
<button className="print-button" onClick={handlePrint}>
|
||||||
🖨️ Печать
|
🖨️ Печать
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<div className="pdf-container" style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%'
|
|
||||||
}}>
|
|
||||||
<Document file={fileUrl} loading="Загрузка PDF...">
|
|
||||||
<div style={{ margin: '0 auto' }}>
|
|
||||||
<Page pageNumber={1} />
|
|
||||||
</div>
|
|
||||||
</Document>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="pdf-container" style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<Document file={fileUrl} loading="Загрузка PDF...">
|
||||||
|
<div style={{ margin: '0 auto' }}>
|
||||||
|
<Page pageNumber={1} />
|
||||||
|
</div>
|
||||||
|
</Document>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PayslipViewer;
|
export default PayslipViewer;
|
||||||
@@ -0,0 +1,642 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
FaUserCircle, FaClock, FaBriefcase, FaAward,
|
||||||
|
FaExclamationTriangle, FaCalendarCheck, FaUtensils,
|
||||||
|
FaFileAlt, FaEdit, FaSave, FaTimes
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface ProfileProps {
|
||||||
|
onBack: () => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция-обертка для рендеринга иконок
|
||||||
|
const renderIcon = (Icon: any, style?: React.CSSProperties) => {
|
||||||
|
return React.createElement(Icon, { style });
|
||||||
|
};
|
||||||
|
|
||||||
|
const Profile: React.FC<ProfileProps> = ({ onBack, employee }) => {
|
||||||
|
const [activeTab, setActiveTab] = useState('about');
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
fullName: employee?.fullName || 'Иванов Иван Иванович',
|
||||||
|
position: employee?.position || 'Инженер-технолог',
|
||||||
|
department: employee?.department || 'Цех №5',
|
||||||
|
phone: employee?.phone || '+7 (999) 123-45-67',
|
||||||
|
email: employee?.email || 'ivanov@company.ru',
|
||||||
|
address: employee?.address || 'г. Москва, ул. Ленина, д. 1, кв. 10',
|
||||||
|
birthDate: employee?.birthDate || '1985-06-15',
|
||||||
|
education: employee?.education || 'Высшее, МГУ им. Ломоносова, химический факультет',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{ id: 'about', label: 'Обо мне', icon: FaUserCircle },
|
||||||
|
{ id: 'achievements', label: 'Достижения', icon: FaAward },
|
||||||
|
{ id: 'vacation', label: 'Отпуск', icon: FaCalendarCheck },
|
||||||
|
{ id: 'meal', label: 'Питание', icon: FaUtensils },
|
||||||
|
{ id: 'certificates', label: 'Справки', icon: FaFileAlt },
|
||||||
|
];
|
||||||
|
|
||||||
|
const achievements = [
|
||||||
|
{ type: 'Поощрение', name: 'Благодарность за успешный проект', date: '2024-01-15' },
|
||||||
|
{ type: 'Поощрение', name: 'Премия по итогам года', date: '2023-12-20' },
|
||||||
|
{ type: 'Взыскание', name: 'Замечание за опоздание', date: '2023-10-01' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTabContent = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'about':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '25px' }}>
|
||||||
|
<div style={{
|
||||||
|
width: '80px',
|
||||||
|
height: '80px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#e8f0fe',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '40px',
|
||||||
|
color: '#34495e',
|
||||||
|
}}>
|
||||||
|
{renderIcon(FaUserCircle)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="fullName"
|
||||||
|
value={formData.fullName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#2d3436',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '5px 10px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{formData.fullName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ fontSize: '14px', color: '#636e72' }}>
|
||||||
|
{formData.position} • {formData.department}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', color: '#b2bec3', marginBottom: '4px' }}>Телефон</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px 10px',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '15px', color: '#2d3436' }}>{formData.phone}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', color: '#b2bec3', marginBottom: '4px' }}>Email</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px 10px',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '15px', color: '#2d3436' }}>{formData.email}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', color: '#b2bec3', marginBottom: '4px' }}>Адрес</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px 10px',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '15px', color: '#2d3436' }}>{formData.address}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '13px', color: '#b2bec3', marginBottom: '4px' }}>Дата рождения</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="birthDate"
|
||||||
|
value={formData.birthDate}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px 10px',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '15px', color: '#2d3436' }}>
|
||||||
|
{new Date(formData.birthDate).toLocaleDateString('ru-RU')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ gridColumn: '1 / -1' }}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#b2bec3', marginBottom: '4px' }}>Образование</div>
|
||||||
|
{isEditing ? (
|
||||||
|
<textarea
|
||||||
|
name="education"
|
||||||
|
value={formData.education}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '5px 10px',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
minHeight: '60px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '15px', color: '#2d3436' }}>{formData.education}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '25px', display: 'flex', gap: '10px' }}>
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
style={{
|
||||||
|
padding: '10px 25px',
|
||||||
|
background: '#1abc9c',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = '#16a085'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = '#1abc9c'}
|
||||||
|
>
|
||||||
|
{renderIcon(FaSave)}
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(false)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 25px',
|
||||||
|
background: '#e74c3c',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = '#c0392b'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = '#e74c3c'}
|
||||||
|
>
|
||||||
|
{renderIcon(FaTimes)}
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 25px',
|
||||||
|
background: '#34495e',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.background = '#2c3e50'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.background = '#34495e'}
|
||||||
|
>
|
||||||
|
{renderIcon(FaEdit)}
|
||||||
|
Редактировать
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'achievements':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: '15px',
|
||||||
|
marginBottom: '25px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
|
||||||
|
{renderIcon(FaClock, { color: '#3498db' })}
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72' }}>Общий стаж</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{employee?.totalExperience || 15} лет
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '15px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
|
||||||
|
{renderIcon(FaBriefcase, { color: '#2ecc71' })}
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72' }}>Стаж на предприятии</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '20px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{employee?.companyExperience || 8} лет
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '16px', fontWeight: 500, color: '#2d3436', marginBottom: '15px' }}>
|
||||||
|
Достижения и взыскания
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
{achievements.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
padding: '12px 15px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
borderRadius: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
|
{item.type === 'Поощрение' ?
|
||||||
|
renderIcon(FaAward, { color: '#f39c12' }) :
|
||||||
|
renderIcon(FaExclamationTriangle, { color: '#e74c3c' })
|
||||||
|
}
|
||||||
|
<span style={{ fontSize: '14px', color: '#2d3436' }}>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '12px', color: '#b2bec3' }}>
|
||||||
|
{new Date(item.date).toLocaleDateString('ru-RU')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'vacation':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
{renderIcon(FaCalendarCheck, { color: '#6c5ce7' })}
|
||||||
|
Информация об отпуске
|
||||||
|
</h3>
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: '1fr 1fr',
|
||||||
|
gap: '15px',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72', marginBottom: '8px' }}>
|
||||||
|
Всего дней отпуска
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '28px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{employee?.vacationDays || 28}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72', marginBottom: '8px' }}>
|
||||||
|
Использовано дней
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '28px', fontWeight: 600, color: '#2d3436' }}>
|
||||||
|
{employee?.usedVacationDays || 14}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
gridColumn: '1 / -1',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '13px', color: '#636e72', marginBottom: '8px' }}>
|
||||||
|
Осталось дней
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '28px', fontWeight: 600, color: '#1abc9c' }}>
|
||||||
|
{(employee?.vacationDays || 28) - (employee?.usedVacationDays || 14)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'meal':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
{renderIcon(FaUtensils, { color: '#f39c12' })}
|
||||||
|
Питание по пропуску
|
||||||
|
</h3>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '15px' }}>
|
||||||
|
<span style={{ color: '#636e72' }}>Статус</span>
|
||||||
|
<span style={{ color: '#2ecc71', fontWeight: 500 }}>
|
||||||
|
{employee?.mealPlan?.hasMealPass ? 'Активен' : 'Неактивен'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{employee?.mealPlan?.hasMealPass && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '15px' }}>
|
||||||
|
<span style={{ color: '#636e72' }}>Номер пропуска</span>
|
||||||
|
<span style={{ color: '#2d3436', fontWeight: 500 }}>
|
||||||
|
{employee.mealPlan.passNumber || 'MP-2024-005'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#636e72' }}>Осталось обедов</span>
|
||||||
|
<span style={{ color: '#2d3436', fontWeight: 500 }}>
|
||||||
|
{employee.mealPlan.remainingMeals || 15}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'certificates':
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
{renderIcon(FaFileAlt, { color: '#6c5ce7' })}
|
||||||
|
Заказ справок
|
||||||
|
</h3>
|
||||||
|
<div style={{
|
||||||
|
background: '#f8f9fa',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}>
|
||||||
|
<p style={{ color: '#636e72', marginBottom: '20px' }}>
|
||||||
|
Вы можете заказать следующие справки:
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
<button style={{
|
||||||
|
padding: '12px',
|
||||||
|
background: 'white',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f0f0f0';
|
||||||
|
e.currentTarget.style.borderColor = '#b2bec3';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'white';
|
||||||
|
e.currentTarget.style.borderColor = '#dfe6e9';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Справка о доходах
|
||||||
|
</button>
|
||||||
|
<button style={{
|
||||||
|
padding: '12px',
|
||||||
|
background: 'white',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f0f0f0';
|
||||||
|
e.currentTarget.style.borderColor = '#b2bec3';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'white';
|
||||||
|
e.currentTarget.style.borderColor = '#dfe6e9';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Справка о трудовом стаже
|
||||||
|
</button>
|
||||||
|
<button style={{
|
||||||
|
padding: '12px',
|
||||||
|
background: 'white',
|
||||||
|
border: '1px solid #dfe6e9',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#f0f0f0';
|
||||||
|
e.currentTarget.style.borderColor = '#b2bec3';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'white';
|
||||||
|
e.currentTarget.style.borderColor = '#dfe6e9';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Справка с места работы
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
paddingBottom: '80px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: '25px' }}>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#5a6268';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#6c757d';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
borderBottom: '1px solid #f1f2f6',
|
||||||
|
overflowX: 'auto',
|
||||||
|
}}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
padding: '15px 20px',
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: activeTab === tab.id ? '3px solid #1abc9c' : '3px solid transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: activeTab === tab.id ? '#1abc9c' : '#636e72',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: activeTab === tab.id ? 600 : 400,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderIcon(tab.icon)}
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '25px' }}>
|
||||||
|
{renderTabContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { FaHandsHelping, FaCalendarCheck, FaMoneyBillWave, FaHome } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface TradeUnionProps {
|
||||||
|
onBack: () => void;
|
||||||
|
employee: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция-обертка для рендеринга иконок
|
||||||
|
const renderIcon = (Icon: any, style?: React.CSSProperties) => {
|
||||||
|
return React.createElement(Icon, { style });
|
||||||
|
};
|
||||||
|
|
||||||
|
const TradeUnion: React.FC<TradeUnionProps> = ({ onBack, employee }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="trade-union-section"
|
||||||
|
style={{
|
||||||
|
padding: '20px',
|
||||||
|
maxWidth: '1200px',
|
||||||
|
margin: '0 auto',
|
||||||
|
paddingBottom: '80px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="top-bar" style={{ marginBottom: '25px' }}>
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={onBack}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
background: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#5a6268';
|
||||||
|
e.currentTarget.style.transform = 'translateX(-2px)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = '#6c757d';
|
||||||
|
e.currentTarget.style.transform = 'translateX(0)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style={{
|
||||||
|
color: '#2d3436',
|
||||||
|
fontSize: '24px',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: '30px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
}}>
|
||||||
|
{renderIcon(FaHandsHelping, { color: '#e74c3c', fontSize: '24px' })}
|
||||||
|
Профсоюз
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||||||
|
gap: '20px',
|
||||||
|
}}>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '25px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
border: '1px solid #f1f2f6',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '32px', color: '#e74c3c', marginBottom: '15px' }}>
|
||||||
|
{renderIcon(FaCalendarCheck)}
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
||||||
|
Мероприятия
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
||||||
|
Анонсы и запись на мероприятия
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '25px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
border: '1px solid #f1f2f6',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '32px', color: '#f39c12', marginBottom: '15px' }}>
|
||||||
|
{renderIcon(FaMoneyBillWave)}
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
||||||
|
Материальная помощь
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
||||||
|
Размеры и виды материальной помощи
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '25px',
|
||||||
|
boxShadow: '0 4px 20px rgba(0,0,0,0.06)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
border: '1px solid #f1f2f6',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '32px', color: '#3498db', marginBottom: '15px' }}>
|
||||||
|
{renderIcon(FaHome)}
|
||||||
|
</div>
|
||||||
|
<h3 style={{ margin: '0 0 10px 0', color: '#2d3436', fontSize: '18px' }}>
|
||||||
|
Жилищные условия
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#636e72', fontSize: '14px', lineHeight: 1.6, margin: 0 }}>
|
||||||
|
Информация об очереди на улучшение жилищных условий
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
marginTop: '30px',
|
||||||
|
padding: '20px',
|
||||||
|
background: '#f8f9fa',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid #f1f2f6',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#636e72',
|
||||||
|
}}>
|
||||||
|
<p style={{ margin: 0 }}>
|
||||||
|
ℹ️ Раздел в разработке. Функционал будет доступен в ближайшее время.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TradeUnion;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface VideoBackgroundProps {
|
||||||
|
onComplete: () => void;
|
||||||
|
videoSrc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoBackground: React.FC<VideoBackgroundProps> = ({
|
||||||
|
onComplete,
|
||||||
|
videoSrc = '/assets/videos/flag.mp4'
|
||||||
|
}) => {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.play().catch(err => {
|
||||||
|
console.warn('Видео не воспроизвелось автоматически:', err);
|
||||||
|
// Если видео не запустилось — показываем экран логина через 5 секунд
|
||||||
|
setTimeout(onComplete, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ Убираем таймер, который закрывал видео через 30 секунд
|
||||||
|
// Теперь видео будет крутиться бесконечно (loop=true)
|
||||||
|
// Только клик или касание пользователя завершают видео
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Очистка не нужна, так как таймера больше нет
|
||||||
|
};
|
||||||
|
}, [onComplete]);
|
||||||
|
|
||||||
|
// ❌ Убираем handleVideoEnd — не нужен, так как loop=true
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
onComplete(); // По клику переходим дальше
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="video-splash"
|
||||||
|
onClick={handleClick}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
background: '#000',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={videoSrc}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
loop // ✅ Бесконечное воспроизведение
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '60px',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: '14px',
|
||||||
|
opacity: 0.6,
|
||||||
|
textShadow: '0 2px 4px rgba(0,0,0,0.5)',
|
||||||
|
}}>
|
||||||
|
Нажмите для продолжения
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoBackground;
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
interface UseActivityTimerProps {
|
||||||
|
timeout: number; // Время до предупреждения (10 сек)
|
||||||
|
warningTimeout: number; // Время до закрытия после предупреждения (5 сек)
|
||||||
|
onInactive: () => void; // Колбэк при закрытии
|
||||||
|
onWarning?: () => void; // Колбэк при показе предупреждения
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useActivityTimer = ({
|
||||||
|
timeout,
|
||||||
|
warningTimeout,
|
||||||
|
onInactive,
|
||||||
|
onWarning
|
||||||
|
}: UseActivityTimerProps) => {
|
||||||
|
// 📊 Состояния
|
||||||
|
const [isWarning, setIsWarning] = useState(false);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(timeout);
|
||||||
|
|
||||||
|
// 📦 Рефы для таймеров
|
||||||
|
const mainTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const warningTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastActivityRef = useRef<number>(Date.now());
|
||||||
|
const onInactiveRef = useRef(onInactive);
|
||||||
|
const onWarningRef = useRef(onWarning);
|
||||||
|
|
||||||
|
// Обновляем ссылки на колбэки
|
||||||
|
useEffect(() => {
|
||||||
|
onInactiveRef.current = onInactive;
|
||||||
|
}, [onInactive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onWarningRef.current = onWarning;
|
||||||
|
}, [onWarning]);
|
||||||
|
|
||||||
|
// 🧹 Очистка всех таймеров
|
||||||
|
const clearAllTimers = useCallback(() => {
|
||||||
|
if (mainTimerRef.current) {
|
||||||
|
clearTimeout(mainTimerRef.current);
|
||||||
|
mainTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (warningTimerRef.current) {
|
||||||
|
clearTimeout(warningTimerRef.current);
|
||||||
|
warningTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
countdownIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 🔄 Сброс таймера (при активности)
|
||||||
|
const resetTimer = useCallback(() => {
|
||||||
|
// Обновляем время последней активности
|
||||||
|
lastActivityRef.current = Date.now();
|
||||||
|
|
||||||
|
// Сбрасываем состояние предупреждения
|
||||||
|
setIsWarning(false);
|
||||||
|
setTimeLeft(timeout);
|
||||||
|
|
||||||
|
// Очищаем все таймеры
|
||||||
|
clearAllTimers();
|
||||||
|
|
||||||
|
console.log(`[ACTIVITY TIMER] 🔄 Таймер сброшен (таймаут: ${timeout}ms)`);
|
||||||
|
|
||||||
|
// ✅ Основной таймер (через timeout мс показываем предупреждение)
|
||||||
|
mainTimerRef.current = setTimeout(() => {
|
||||||
|
console.log(`[ACTIVITY TIMER] ⚠️ Показываем предупреждение!`);
|
||||||
|
|
||||||
|
// Показываем предупреждение
|
||||||
|
setIsWarning(true);
|
||||||
|
setTimeLeft(warningTimeout);
|
||||||
|
|
||||||
|
// Вызываем колбэк предупреждения
|
||||||
|
if (onWarningRef.current) {
|
||||||
|
onWarningRef.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 Таймер обратного отсчета (обновляем каждую секунду)
|
||||||
|
countdownIntervalRef.current = setInterval(() => {
|
||||||
|
setTimeLeft(prev => {
|
||||||
|
const newTime = prev - 1000;
|
||||||
|
console.log(`[ACTIVITY TIMER] ⏰ Осталось: ${newTime/1000} сек`);
|
||||||
|
return Math.max(0, newTime);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// ✅ Таймер закрытия (через warningTimeout мс)
|
||||||
|
warningTimerRef.current = setTimeout(() => {
|
||||||
|
console.log(`[ACTIVITY TIMER] 🛑 Время вышло! Вызываем onInactive`);
|
||||||
|
|
||||||
|
// Очищаем интервал
|
||||||
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
countdownIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем колбэк
|
||||||
|
onInactiveRef.current();
|
||||||
|
|
||||||
|
}, warningTimeout);
|
||||||
|
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
}, [timeout, warningTimeout, clearAllTimers]);
|
||||||
|
|
||||||
|
// 🖱️ Обработчик активности
|
||||||
|
useEffect(() => {
|
||||||
|
// События, которые считаются активностью
|
||||||
|
const events = [
|
||||||
|
'mousedown',
|
||||||
|
'mousemove',
|
||||||
|
'keydown',
|
||||||
|
'scroll',
|
||||||
|
'touchstart',
|
||||||
|
'click',
|
||||||
|
'wheel',
|
||||||
|
'touchmove',
|
||||||
|
'focus'
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleActivity = () => {
|
||||||
|
// Если было предупреждение — сбрасываем
|
||||||
|
if (isWarning) {
|
||||||
|
console.log('[ACTIVITY TIMER] 👆 Активность! Сбрасываем предупреждение');
|
||||||
|
}
|
||||||
|
resetTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавляем обработчики
|
||||||
|
events.forEach(event => {
|
||||||
|
document.addEventListener(event, handleActivity, { passive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Запускаем таймер
|
||||||
|
resetTimer();
|
||||||
|
|
||||||
|
console.log(`[ACTIVITY TIMER] 🚀 Инициализирован (таймаут: ${timeout}ms, предупреждение: ${warningTimeout}ms)`);
|
||||||
|
|
||||||
|
// Очистка
|
||||||
|
return () => {
|
||||||
|
events.forEach(event => {
|
||||||
|
document.removeEventListener(event, handleActivity);
|
||||||
|
});
|
||||||
|
clearAllTimers();
|
||||||
|
console.log('[ACTIVITY TIMER] 🧹 Очищен');
|
||||||
|
};
|
||||||
|
}, [resetTimer, clearAllTimers]);
|
||||||
|
|
||||||
|
// ✅ Возвращаем состояние для отображения в UI
|
||||||
|
return {
|
||||||
|
isWarning, // ⚠️ Показывать ли предупреждение
|
||||||
|
timeLeft, // ⏰ Оставшееся время в мс
|
||||||
|
resetTimer // 🔄 Ручной сброс
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useActivityTimer;
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/* ============================================
|
||||||
|
АНИМАЦИИ
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Плавное появление */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Плавное появление с задержкой */
|
||||||
|
.fade-in-delay {
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пульсация */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Вращение (для спиннера) */
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скелетон загрузки */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скольжение снизу */
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up {
|
||||||
|
animation: slideUp 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скольжение сверху */
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down {
|
||||||
|
animation: slideDown 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Масштабирование */
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-in {
|
||||||
|
animation: scaleIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Бегущая строка */
|
||||||
|
@keyframes ticker {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker {
|
||||||
|
animation: ticker 20s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticker:hover {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Затемнение фона */
|
||||||
|
@keyframes overlayIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-in {
|
||||||
|
animation: overlayIn 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Всплывающая подсказка */
|
||||||
|
@keyframes tooltipIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-in {
|
||||||
|
animation: tooltipIn 0.2s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Затухание */
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-out {
|
||||||
|
animation: fadeOut 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пульсирующее предупреждение */
|
||||||
|
@keyframes pulseWarning {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.03);
|
||||||
|
box-shadow: 0 20px 80px rgba(255, 193, 7, 0.5);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мигающий текст обратного отсчета */
|
||||||
|
@keyframes blinkText {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Карта */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для плавного появления/исчезновения контролов */
|
||||||
|
@keyframes fadeSlideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeSlideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+449
-98
@@ -1,4 +1,10 @@
|
|||||||
/* Общие стили */
|
/* Общие стили */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -8,9 +14,12 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Окно логина */
|
/* ============================================
|
||||||
|
ОКНО ЛОГИНА
|
||||||
|
============================================ */
|
||||||
.login-window {
|
.login-window {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 12% auto;
|
margin: 12% auto;
|
||||||
@@ -18,7 +27,6 @@ body {
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-window h2 {
|
.login-window h2 {
|
||||||
@@ -83,7 +91,199 @@ button:disabled {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Информация о сотруднике */
|
/* ============================================
|
||||||
|
ГЛАВНОЕ МЕНЮ
|
||||||
|
============================================ */
|
||||||
|
.main-menu {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-menu h1 {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 200px));
|
||||||
|
gap: 25px;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 25px 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
transform: translateY(-5px) scale(1.05);
|
||||||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #2d3436;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ВИДЕО-ЗАСТАВКА
|
||||||
|
============================================ */
|
||||||
|
.video-splash {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-splash video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-splash .hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
НИЖНЯЯ ПАНЕЛЬ (КУРСЫ ВАЛЮТ)
|
||||||
|
============================================ */
|
||||||
|
.bottom-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(44, 62, 80, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .status .online {
|
||||||
|
color: #2ecc71;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .status .offline {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .status .label {
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates {
|
||||||
|
display: flex;
|
||||||
|
gap: 25px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates .item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates .item .currency {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates .item .change-up {
|
||||||
|
color: #2ecc71;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates .item .change-down {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates .divider {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .time {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ИНФОРМАЦИЯ О СОТРУДНИКЕ
|
||||||
|
============================================ */
|
||||||
.employee-info {
|
.employee-info {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
@@ -128,7 +328,9 @@ button:disabled {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Секция расчётных листов */
|
/* ============================================
|
||||||
|
РАСЧЁТНЫЙ ЛИСТ
|
||||||
|
============================================ */
|
||||||
.payslip-section {
|
.payslip-section {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -170,33 +372,16 @@ button:disabled {
|
|||||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
/* ============================================
|
||||||
display: flex;
|
ВЕРХНЯЯ ПАНЕЛЬ (КНОПКА НАЗАД)
|
||||||
gap: 10px;
|
============================================ */
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button {
|
|
||||||
background-color: #28a745;
|
|
||||||
color: white;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button:hover {
|
|
||||||
background-color: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button:first-child {
|
|
||||||
background-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons button:first-child:hover {
|
|
||||||
background-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Верхняя панель с кнопкой назад */
|
|
||||||
.top-bar {
|
.top-bar {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
@@ -210,20 +395,19 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.2s ease;
|
||||||
}
|
|
||||||
|
|
||||||
.back-button:hover {
|
|
||||||
background-color: #545b62;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Делаем кнопку назад компактнее */
|
|
||||||
.back-button {
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Гостевой режим */
|
.back-button:hover {
|
||||||
|
background-color: #545b62;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ГОСТЕВОЙ РЕЖИМ
|
||||||
|
============================================ */
|
||||||
.guest-mode {
|
.guest-mode {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
@@ -287,7 +471,6 @@ button:disabled {
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -308,94 +491,262 @@ button:disabled {
|
|||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Handbook Content Styles --- */
|
/* ============================================
|
||||||
|
HANDBOOK СТИЛИ
|
||||||
|
============================================ */
|
||||||
.handbook-content {
|
.handbook-content {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 12px;
|
padding: 24px 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
.handbook-content h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2rem;
|
|
||||||
margin: 24px 0 16px 0;
|
|
||||||
color: #1a237e;
|
|
||||||
}
|
|
||||||
.handbook-content h2 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 20px 0 12px 0;
|
|
||||||
color: #283593;
|
|
||||||
}
|
|
||||||
.handbook-content p {
|
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 1.08rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.handbook-content ul {
|
|
||||||
margin: 12px 0 12px 32px;
|
|
||||||
padding-left: 18px;
|
|
||||||
}
|
|
||||||
.handbook-content li {
|
|
||||||
margin: 6px 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.handbook-content strong {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #000000;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
|
.handbook-content h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 24px 0 16px 0;
|
||||||
|
color: #1a237e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handbook-content h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 20px 0 12px 0;
|
||||||
|
color: #283593;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handbook-content p {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handbook-content ul {
|
||||||
|
margin: 12px 0 12px 32px;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handbook-content li {
|
||||||
|
margin: 6px 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handbook-content strong {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000000;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content h3 {
|
.handbook-content h3 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content em {
|
.handbook-content em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #555;
|
color: #555;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content h4 {
|
.handbook-content h4 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-table {
|
.handbook-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-table th,
|
.handbook-table th,
|
||||||
.handbook-table td {
|
.handbook-table td {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-table th {
|
.handbook-table th {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content table,
|
.handbook-content table,
|
||||||
.handbook-content th,
|
.handbook-content th,
|
||||||
.handbook-content td,
|
.handbook-content td,
|
||||||
.handbook-content tr {
|
.handbook-content tr {
|
||||||
border: 1px solid #bdbdbd;
|
border: 1px solid #bdbdbd;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content th,
|
.handbook-content th,
|
||||||
.handbook-content td {
|
.handbook-content td {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.handbook-content table {
|
.handbook-content table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
РАЗДЕЛЫ (Finance, Medicine, TradeUnion и т.д.)
|
||||||
|
============================================ */
|
||||||
|
.section-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: #2d3436;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid #f1f2f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card .card-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card .card-title {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #2d3436;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card .card-description {
|
||||||
|
color: #636e72;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ИНФО-БЛОК
|
||||||
|
============================================ */
|
||||||
|
.info-block {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #f1f2f6;
|
||||||
|
text-align: center;
|
||||||
|
color: #636e72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-block p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
АДАПТИВНОСТЬ
|
||||||
|
============================================ */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-window {
|
||||||
|
margin: 20% 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.employee-photo {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-menu h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 150px));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-navigation {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
height: 34px;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-bar .rates {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-window {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 120px));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 15px 10px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item .title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user