admin_to_react
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from 'react';
|
||||
import './styles/main.css';
|
||||
import LoginWindow from './components/LoginWindow';
|
||||
import EmployeeInfo from './components/EmployeeInfo';
|
||||
import GuestMode from './components/GuestMode';
|
||||
import PayslipViewer from './components/PayslipViewer';
|
||||
import config from './config.json';
|
||||
|
||||
interface Employee {
|
||||
id: string;
|
||||
tnom: string;
|
||||
famaly: string;
|
||||
ima: string;
|
||||
otch: string;
|
||||
category: string;
|
||||
organization: string;
|
||||
identNumber: string;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [isGuestMode, setIsGuestMode] = useState(false);
|
||||
const [employee, setEmployee] = useState<Employee | null>(null);
|
||||
const [showPayslip, setShowPayslip] = useState(false);
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('');
|
||||
const [loginError, setLoginError] = useState('');
|
||||
|
||||
const handleLogin = async (id: string) => {
|
||||
try {
|
||||
const isCardUid = /^\d{7}$/.test(id);
|
||||
const url = isCardUid
|
||||
? `${config.backendUrl}/api/employee/card/${id}`
|
||||
: `${config.backendUrl}/api/employee/${id}`;
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setEmployee(data);
|
||||
setIsLoggedIn(true);
|
||||
setLoginError('');
|
||||
} else {
|
||||
setLoginError('Сотрудник не найден');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setLoginError('Ошибка при получении данных');
|
||||
}
|
||||
};
|
||||
|
||||
const handleGuestMode = () => {
|
||||
setIsGuestMode(true);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setIsLoggedIn(false);
|
||||
setEmployee(null);
|
||||
setIsGuestMode(false);
|
||||
setShowPayslip(false);
|
||||
setSelectedPeriod('');
|
||||
};
|
||||
|
||||
const handleViewPayslip = (period: string) => {
|
||||
setSelectedPeriod(period);
|
||||
setShowPayslip(true);
|
||||
};
|
||||
|
||||
const handlePayslipBack = () => {
|
||||
setShowPayslip(false);
|
||||
setSelectedPeriod('');
|
||||
};
|
||||
|
||||
const getPayslipUrl = (employee: Employee, period: string) => {
|
||||
const fileName = `${employee.tnom}.pdf`;
|
||||
|
||||
return `${config.backendUrl}/api/payslip/${period}/${fileName}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{!isLoggedIn && !isGuestMode ? (
|
||||
<LoginWindow onLogin={handleLogin} onGuestMode={handleGuestMode} loginError={loginError} />
|
||||
) : isGuestMode ? (
|
||||
<GuestMode onBack={handleBack} />
|
||||
) : showPayslip ? (
|
||||
<PayslipViewer
|
||||
fileUrl={employee && selectedPeriod ? getPayslipUrl(employee, selectedPeriod) : "/payslips/test.pdf"}
|
||||
onBack={handlePayslipBack}
|
||||
/>
|
||||
) : employee ? (
|
||||
<EmployeeInfo
|
||||
employee={employee}
|
||||
onViewPayslip={handleViewPayslip}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Employee {
|
||||
id: string;
|
||||
cardNumber?: string;
|
||||
tnom: string;
|
||||
famaly: string;
|
||||
ima: string;
|
||||
otch: string;
|
||||
profession?: string;
|
||||
category?: string;
|
||||
departmentName?: string;
|
||||
organization: string;
|
||||
}
|
||||
|
||||
interface EmployeeInfoProps {
|
||||
employee: Employee;
|
||||
onViewPayslip: (period: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const EmployeeInfo: React.FC<EmployeeInfoProps> = ({
|
||||
employee,
|
||||
onViewPayslip,
|
||||
onBack
|
||||
}) => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState(() => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return `${month}${year}`;
|
||||
});
|
||||
|
||||
// Генерируем список периодов за последние 12 месяцев
|
||||
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 = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||||
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
|
||||
|
||||
return (
|
||||
<div className="employee-info">
|
||||
<div className="top-bar">
|
||||
<button className="back-button" onClick={onBack}>
|
||||
← Назад
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="employee-header">
|
||||
<div className="employee-photo">
|
||||
<div className="photo-placeholder">Фото</div>
|
||||
</div>
|
||||
<div className="employee-details">
|
||||
<h2>{`${employee.famaly} ${employee.ima} ${employee.otch}`}</h2>
|
||||
<p>Табельный номер: {employee.tnom}</p>
|
||||
{employee.category?.trim() && <p>Категория: {employee.category}</p>}
|
||||
{employee.departmentName && (
|
||||
<p>Подразделение: {employee.departmentName}</p>
|
||||
)}
|
||||
<p>Организация: {employee.organization}</p>
|
||||
{employee.profession && <p>Профессия: {employee.profession}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payslip-section">
|
||||
<h3>Расчётный лист</h3>
|
||||
<div className="period-selector">
|
||||
<label htmlFor="period-select">Выберите период:</label>
|
||||
<select
|
||||
id="period-select"
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(e.target.value)}
|
||||
>
|
||||
{periods.map(period => (
|
||||
<option key={period} value={period}>
|
||||
{monthNames[parseInt(period.substring(0, 2)) - 1]} {period.substring(2)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button onClick={() => onViewPayslip(selectedPeriod)}>
|
||||
Посмотреть расчётный лист
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeInfo;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import config from "../../src/config.json";
|
||||
interface GuestModeProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
function parseHandbookBlocks(html: string) {
|
||||
const blocks: (string | { type: 'phones', rows: { desc: string, internal: string, city: string }[] })[] = [];
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = html;
|
||||
let phoneRows: { desc: string, internal: string, city: string }[] = [];
|
||||
let lastDesc = '';
|
||||
const phoneRegex = /(\d{2,3}-\d{2,3}(?:-\d{2,3})?|\d{4}\s\d{6,7})/g;
|
||||
const children = Array.from(container.childNodes);
|
||||
let htmlBuffer = '';
|
||||
const flushHtmlBuffer = () => {
|
||||
if (htmlBuffer) {
|
||||
blocks.push(htmlBuffer);
|
||||
htmlBuffer = '';
|
||||
}
|
||||
};
|
||||
const flushPhoneRows = () => {
|
||||
if (phoneRows.length > 1) {
|
||||
blocks.push({ type: 'phones', rows: phoneRows });
|
||||
} else if (phoneRows.length === 1) {
|
||||
const row = phoneRows[0];
|
||||
blocks.push(`<p>${row.desc} ${row.internal}${row.city ? ' ' + row.city : ''}</p>`);
|
||||
}
|
||||
phoneRows = [];
|
||||
};
|
||||
children.forEach(node => {
|
||||
if (node.nodeType === 1) {
|
||||
const tag = (node as HTMLElement).tagName.toLowerCase();
|
||||
const text = (node as HTMLElement).textContent?.trim() || '';
|
||||
const phones = Array.from(text.matchAll(phoneRegex), m => m[1]);
|
||||
if ((tag === 'p' || tag === 'li') && phones.length >= 1) {
|
||||
flushHtmlBuffer();
|
||||
if (phones.length === 2) {
|
||||
const desc = text.replace(phoneRegex, '').trim();
|
||||
phoneRows.push({ desc, internal: phones[0], city: phones[1] });
|
||||
lastDesc = desc;
|
||||
} else if (phones.length === 1) {
|
||||
phoneRows.push({ desc: lastDesc, internal: phones[0], city: '' });
|
||||
} else if (text && !phones.length) {
|
||||
lastDesc = text;
|
||||
}
|
||||
} else {
|
||||
if (phoneRows.length) {
|
||||
flushPhoneRows();
|
||||
}
|
||||
if ((node as HTMLElement).outerHTML) {
|
||||
htmlBuffer += (node as HTMLElement).outerHTML;
|
||||
} else {
|
||||
htmlBuffer += node.textContent || '';
|
||||
}
|
||||
}
|
||||
} else if (node.nodeType === 3) {
|
||||
htmlBuffer += node.textContent;
|
||||
}
|
||||
});
|
||||
if (phoneRows.length) {
|
||||
flushPhoneRows();
|
||||
}
|
||||
if (htmlBuffer) {
|
||||
blocks.push(htmlBuffer);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
const HandbookBlocks: React.FC<{ html: string }> = ({ html }) => {
|
||||
const [blocks, setBlocks] = useState<(string | { type: 'phones', rows: { desc: string, internal: string, city: string }[] })[]>([]);
|
||||
useEffect(() => {
|
||||
setBlocks(parseHandbookBlocks(html));
|
||||
}, [html]);
|
||||
return (
|
||||
<>
|
||||
{blocks.map((block, i) => {
|
||||
if (typeof block === 'string') {
|
||||
return <div key={i} dangerouslySetInnerHTML={{ __html: block }} />;
|
||||
} else if (block.type === 'phones') {
|
||||
return (
|
||||
<table className="handbook-table" key={i} style={{ margin: '24px auto' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Описание</th>
|
||||
<th>АТС предприятия</th>
|
||||
<th>Городская АТС</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row, j) => (
|
||||
<tr key={j}>
|
||||
<td>{row.desc}</td>
|
||||
<td>{row.internal}</td>
|
||||
<td>{row.city}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ADMIN_SERVER_URL = config.adminServerUrl;
|
||||
|
||||
function patchMediaSrc(html: string): string {
|
||||
return html
|
||||
.replace(/<img([^>]+)src=["'](\/uploads\/[^"']+)["']/g, `<img$1src="${ADMIN_SERVER_URL}$2"`)
|
||||
.replace(/<video([^>]+)src=["'](\/uploads\/[^"']+)["']/g, `<video$1src="${ADMIN_SERVER_URL}$2"`);
|
||||
}
|
||||
|
||||
const renderContentBlocks = (blocks: any[]) => (
|
||||
<>
|
||||
{blocks.map((block, i) => (
|
||||
<div key={block.id || i} style={{marginBottom: 16}} dangerouslySetInnerHTML={{ __html: patchMediaSrc(block.content) }} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const GuestMode: React.FC<GuestModeProps> = ({ onBack }) => {
|
||||
const [activeTab, setActiveTab] = useState<'handbook' | 'union' | 'info'>('handbook');
|
||||
const [handbookContent, setHandbookContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [infoBlocks, setInfoBlocks] = useState<any[]>([]);
|
||||
const [unionBlocks, setUnionBlocks] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'handbook') {
|
||||
loadHandbook();
|
||||
} else if (activeTab === 'info') {
|
||||
loadInfoBlocks();
|
||||
} else if (activeTab === 'union') {
|
||||
loadUnionBlocks();
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [activeTab]);
|
||||
|
||||
const loadHandbook = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${config.backendUrl}/api/handbook/html`);
|
||||
const html = await response.text();
|
||||
setHandbookContent(html);
|
||||
} catch (error) {
|
||||
setHandbookContent('Ошибка загрузки справочника');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loadInfoBlocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${ADMIN_SERVER_URL}/moders/content/info`);
|
||||
const data = await res.json();
|
||||
setInfoBlocks(Array.isArray(data) ? data : []);
|
||||
} catch (e) {
|
||||
setInfoBlocks([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loadUnionBlocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${ADMIN_SERVER_URL}/moders/content/union`);
|
||||
const data = await res.json();
|
||||
setUnionBlocks(Array.isArray(data) ? data : []);
|
||||
} catch (e) {
|
||||
setUnionBlocks([]);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="guest-mode">
|
||||
<div className="top-bar">
|
||||
<button className="back-button" onClick={onBack}>
|
||||
← Назад
|
||||
</button>
|
||||
</div>
|
||||
<div className="guest-content">
|
||||
<h2>Гостевой режим</h2>
|
||||
<div className="tab-navigation">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'handbook' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('handbook')}
|
||||
>
|
||||
Справочник
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'union' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('union')}
|
||||
>
|
||||
Профсоюз
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('info')}
|
||||
>
|
||||
Общая информация
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
{activeTab === 'handbook' && (
|
||||
<div className="handbook-content">
|
||||
{loading ? (
|
||||
<div className="loading">Загрузка справочника...</div>
|
||||
) : (
|
||||
<HandbookBlocks html={handbookContent} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'union' && (
|
||||
<div className="union-content">
|
||||
{loading ? (
|
||||
<div className="loading">Загрузка...</div>
|
||||
) : (
|
||||
unionBlocks.length ? renderContentBlocks(unionBlocks) : <p>Нет информации о профсоюзе.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'info' && (
|
||||
<div className="info-content">
|
||||
{loading ? (
|
||||
<div className="loading">Загрузка...</div>
|
||||
) : (
|
||||
infoBlocks.length ? renderContentBlocks(infoBlocks) : <p>Нет общей информации.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuestMode;
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
interface LoginWindowProps {
|
||||
onLogin: (id: string) => void;
|
||||
onGuestMode: () => void;
|
||||
loginError?: string;
|
||||
}
|
||||
const LoginWindow: React.FC<LoginWindowProps> = ({ onLogin, onGuestMode, loginError }) => {
|
||||
const [id, setId] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3001/api/test', {
|
||||
mode: 'cors', // Явно указываем режим CORS
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Backend response:', data);
|
||||
} else {
|
||||
console.error('Backend returned error status:', response.status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Connection test failed:', err);
|
||||
}
|
||||
};
|
||||
testConnection();
|
||||
const setupCardListener = () => {
|
||||
console.log('[LoginWindow] Setting up card listener...');
|
||||
const handleCardDetected = (uid: string) => {
|
||||
console.log('Card detected in renderer via preload:', uid);
|
||||
onLogin(uid);
|
||||
};
|
||||
|
||||
window.electronAPI.onCardDetected(handleCardDetected);
|
||||
};
|
||||
if (window.electronAPI) {
|
||||
setupCardListener();
|
||||
} else {
|
||||
console.log('[LoginWindow] electronAPI not ready, waiting for event...');
|
||||
window.addEventListener('electronApiReady', setupCardListener, { once: true });
|
||||
}
|
||||
return () => {
|
||||
console.log('[LoginWindow] Cleaning up listeners.');
|
||||
window.removeEventListener('electronApiReady', setupCardListener);
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.removeCardDetectedListeners();
|
||||
}
|
||||
};
|
||||
}, [onLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginError) {
|
||||
setId('');
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [loginError]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (id.trim()) {
|
||||
onLogin(id.trim());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-window">
|
||||
<h2>Вход в систему</h2>
|
||||
<div className="card-prompt">
|
||||
<p>Пожалуйста, приложите вашу карту пропуска к считывателю</p>
|
||||
<div className="card-icon">💳</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="input-group">
|
||||
<label htmlFor="id"></label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
id="id"
|
||||
value={id}
|
||||
onChange={(e) => setId(e.target.value)}
|
||||
placeholder=" "
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{loginError && <div className="error" style={{ color: 'red', marginTop: 8 }}>{loginError}</div>}
|
||||
<div className="button-group">
|
||||
<button type="submit" disabled={!id.trim()}>
|
||||
Войти
|
||||
</button>
|
||||
<button type="button" onClick={onGuestMode}>
|
||||
Гостевой режим
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginWindow;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
interface PayslipViewerProps {
|
||||
fileUrl: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const PayslipViewer: React.FC<PayslipViewerProps> = ({ fileUrl, onBack }) => {
|
||||
const handlePrint = () => {
|
||||
const win = window.open(fileUrl, '_blank');
|
||||
if (win) {
|
||||
win.focus();
|
||||
win.print();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="payslip-viewer">
|
||||
<div className="top-bar">
|
||||
<button className="back-button" onClick={onBack}>
|
||||
← Назад
|
||||
</button>
|
||||
<button className="print-button" onClick={handlePrint}>
|
||||
🖨️ Печать
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayslipViewer;
|
||||
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
backendUrl: 'http://localhost:3001',
|
||||
adminUrl: 'http://localhost:3005'
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"backendUrl": "http://localhost:3001",
|
||||
"adminUrl" : "http://localhost:3005",
|
||||
"frontUrl" : "http://localhost:3000",
|
||||
"adminServerUrl": "http://localhost:3005"
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
declare module '*.png';
|
||||
declare module '*.svg';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.gif';
|
||||
declare module '*.bmp';
|
||||
declare module '*.tiff';
|
||||
|
||||
declare module 'fs';
|
||||
declare module 'path';
|
||||
declare module 'os';
|
||||
|
||||
interface Config {
|
||||
backendUrl: string;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: {
|
||||
getAppConfig: () => Promise<Config>;
|
||||
onCardDetected: (callback: (uid: string) => void) => void;
|
||||
removeCardDetectedListeners: () => void;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
declare global {
|
||||
interface PromiseConstructor {
|
||||
withResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!Promise.withResolvers) {
|
||||
Promise.withResolvers = function<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
} {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: any) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
}
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,154 @@
|
||||
const ffi = require('ffi-napi');
|
||||
const ref = require('ref-napi');
|
||||
const Struct = require('ref-struct-napi');
|
||||
const ArrayType = require('ref-array-napi');
|
||||
|
||||
|
||||
const E_RG_ENDPOINT_TYPE = {
|
||||
ET_UNKNOWN: 0,
|
||||
ET_USBHID: 1,
|
||||
ET_SERIAL: 2
|
||||
};
|
||||
const E_RG_STATUS_TYPE = {
|
||||
STE_UNKNOWN: 0,
|
||||
STE_NO_CARD: 1,
|
||||
STE_CARD: 9,
|
||||
STE_CARD_NO_AUTH: 10,
|
||||
STE_CARD_AUTH: 26
|
||||
};
|
||||
const E_RG_CARD_FAMILY_CODE = {
|
||||
CF_PIN: 1,
|
||||
CF_TEMIC: 2,
|
||||
CF_HID: 4,
|
||||
CF_EM: 8,
|
||||
CF_INDALA: 16,
|
||||
CF_COTAG: 32,
|
||||
CF_MIFARE: 64,
|
||||
CF_INDALA_MT: 128,
|
||||
CF_ALL: 255
|
||||
};
|
||||
const RG_CARD_AUTH_PARAMS = Struct({
|
||||
flags: 'uint8',
|
||||
classicKey: ArrayType('uint8', 6),
|
||||
plusKey: ArrayType('uint8', 16)
|
||||
});
|
||||
|
||||
const RG_CARD_MEMORY = Struct({
|
||||
profile_block: 'uint8',
|
||||
block_data: ArrayType('uint8', 16)
|
||||
});
|
||||
const E_API_ERROR_CODES = {
|
||||
EC_OK: 0,
|
||||
EC_FAIL: 1,
|
||||
EC_NOT_IMPLEMENTED: 2,
|
||||
EC_BAD_ARGUMENT: 3,
|
||||
EC_INVALID_HANDLE: 4,
|
||||
EC_INVALID_RESOURCE: 5,
|
||||
EC_INVALID_CONNECTION_TYPE: 6,
|
||||
EC_INVALID_CONNECTION_ADDRESS: 7,
|
||||
EC_INVALID_DEVICE_ADDRESS: 8,
|
||||
EC_DEVICE_OPERATION_UNSUPPORTED: 9,
|
||||
EC_DEVICE_NOT_CONNECTED: 10,
|
||||
EC_DEVICE_NO_RESPOND: 11,
|
||||
EC_DEVICE_COMM_FAILURE: 12,
|
||||
EC_DEVICE_PROTOCOL_FAILURE: 13,
|
||||
EC_POLL_NO_EVENTS: 14,
|
||||
EC_POLL_QUEUE_CLOSED: 15,
|
||||
EC_CALL_INIT: 16,
|
||||
EC_DEVICE_INVALID_COMMAND: 17,
|
||||
EC_DEVICE_INVALID_PARAM: 18,
|
||||
EC_DEVICE_INVALID_PIN: 19,
|
||||
EC_DEVICE_COMMAND_TIMEOUT: 20,
|
||||
EC_DEVICE_NO_CARD: 21,
|
||||
EC_DEVICE_UOWN_CARD: 22,
|
||||
EC_DEVICE_INCOMPATIBLE_CARD: 23,
|
||||
EC_DEVICE_AUTH_FAIL: 24,
|
||||
EC_DEVICE_PROFILE_FAIL: 25,
|
||||
EC_DEVICE_RW_FAIL: 26,
|
||||
EC_IO_OPEN_FAIL: 27,
|
||||
EC_IO_CLOSE_FAIL: 28,
|
||||
EC_IO_READ_FAIL: 29,
|
||||
EC_IO_WRITE_FAIL: 30,
|
||||
EC_IO_CLOSED: 31,
|
||||
EC_DEVICE_IN_BOOT: 32,
|
||||
EC_DEVICE_FW_INVALID_MODEL: 33,
|
||||
EC_FILE_NOT_FOUND: 34,
|
||||
EC_FINGERPRINT_UNFOUND: 35
|
||||
};
|
||||
|
||||
const E_RG_DEVICE_EVENT_TYPE = {
|
||||
DET_UNKNOWN_EVENT: 0,
|
||||
DET_CARD_PLACED_EVENT: 2,
|
||||
DET_CARD_REMOVED_EVENT: 4,
|
||||
DET_RELAY_STATE_CHANGED: 8,
|
||||
DET_TAMPER_STATE_CHANGED: 16,
|
||||
DET_BUTTON_STATE_CHANGED: 32,
|
||||
DET_DOOR_SENSOR_STATE_CHANGED: 64,
|
||||
DET_POLL_ERROR: 128
|
||||
};
|
||||
|
||||
// Структуры
|
||||
const RG_ENDPOINT = Struct({
|
||||
type: 'uint8',
|
||||
address: 'string' // const char*
|
||||
});
|
||||
const RG_CARD_INFO = Struct({
|
||||
type: 'uint8',
|
||||
uid: ArrayType('uint8', 7)
|
||||
});
|
||||
const RG_ENDPOINT_INFO = Struct({
|
||||
type: 'uint8',
|
||||
address: ArrayType('char', 64),
|
||||
friendly_name: ArrayType('char', 128)
|
||||
});
|
||||
const RG_DEVICE_INFO_EXT = Struct({
|
||||
address: 'uint8',
|
||||
serial: 'uint32',
|
||||
firmwareUpdateLock: 'uint8',
|
||||
type: 'uint8',
|
||||
firmware: 'uint16',
|
||||
capabilities: 'uint32'
|
||||
});
|
||||
|
||||
// DLL
|
||||
const rgsec = ffi.Library('librgsec.dll', {
|
||||
'RG_InitializeLib': ['uint32', []],
|
||||
'RG_Uninitialize': ['uint32', []],
|
||||
'RG_FindEndPoints': ['uint32', ['pointer', 'uint8', 'pointer']],
|
||||
'RG_GetFoundEndPointInfo': ['uint32', ['pointer', 'uint32', 'pointer']],
|
||||
'RG_InitDevice': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8']],
|
||||
'RG_SetCardsMask': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8', 'uint8']],
|
||||
'RG_GetStatus': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8', 'pointer', 'pointer', ref.refType(RG_CARD_INFO), 'pointer']],
|
||||
'RG_CloseResource': ['uint32', ['pointer']],
|
||||
'RG_WriteProfile': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8', 'uint8', 'uint8', ref.refType(RG_CARD_AUTH_PARAMS)]],
|
||||
'RG_ReadBlockDirect': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8', ref.refType(RG_CARD_MEMORY), ref.refType(RG_CARD_AUTH_PARAMS)]],
|
||||
'RG_Subscribe': ['uint32', ['pointer', ref.refType(RG_ENDPOINT), 'uint8', 'uint32', 'pointer']],
|
||||
'RG_PollEvents': ['uint32', ['pointer', 'pointer', 'pointer', 'uint32']],
|
||||
'RG_IsolateDevice': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8']],
|
||||
'RG_ResetIsolation': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8']],
|
||||
'RG_CloseDevice': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8']],
|
||||
'RG_GetInfoExt': ['uint32', [ref.refType(RG_ENDPOINT), 'uint8', 'pointer']],
|
||||
'RG_Subscribe': ['uint32', ['pointer', ref.refType(RG_ENDPOINT), 'uint8', 'uint32', 'pointer']],
|
||||
|
||||
});
|
||||
|
||||
function errorToString(code) {
|
||||
for (const [k, v] of Object.entries(E_API_ERROR_CODES)) {
|
||||
if (v === code) return k;
|
||||
}
|
||||
return `Unknown error (${code})`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
E_RG_ENDPOINT_TYPE,
|
||||
E_RG_STATUS_TYPE,
|
||||
E_RG_CARD_FAMILY_CODE,
|
||||
E_API_ERROR_CODES,
|
||||
E_RG_DEVICE_EVENT_TYPE,
|
||||
RG_DEVICE_INFO_EXT,
|
||||
RG_ENDPOINT,
|
||||
RG_CARD_INFO,
|
||||
RG_ENDPOINT_INFO,
|
||||
rgsec,
|
||||
errorToString
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
const ref = require('ref-napi');
|
||||
const Struct = require('ref-struct-napi');
|
||||
const {
|
||||
E_RG_ENDPOINT_TYPE,
|
||||
E_RG_DEVICE_EVENT_TYPE,
|
||||
E_API_ERROR_CODES,
|
||||
RG_ENDPOINT,
|
||||
RG_ENDPOINT_INFO,
|
||||
RG_CARD_INFO,
|
||||
RG_DEVICE_INFO_EXT,
|
||||
rgsec,
|
||||
errorToString
|
||||
} = require('./r5usb');
|
||||
const axios = require('axios');
|
||||
|
||||
function readCString(buffer, size) {
|
||||
const bytes = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
const byte = buffer.readUInt8(i);
|
||||
if (byte === 0) break;
|
||||
bytes.push(byte);
|
||||
}
|
||||
return Buffer.from(bytes).toString('utf8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Старт программы');
|
||||
const initRes = rgsec.RG_InitializeLib();
|
||||
console.log('После RG_InitializeLib, код:', initRes);
|
||||
if (initRes !== E_API_ERROR_CODES.EC_OK) {
|
||||
console.error('RG_InitializeLib failed:', errorToString(initRes));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Поиск устройств...');
|
||||
const endpointListPtr = ref.alloc('pointer');
|
||||
const countPtr = ref.alloc('uint32');
|
||||
const findRes = rgsec.RG_FindEndPoints(
|
||||
endpointListPtr,
|
||||
E_RG_ENDPOINT_TYPE.ET_USBHID,
|
||||
countPtr
|
||||
);
|
||||
console.log('После RG_FindEndPoints, код:', findRes);
|
||||
|
||||
if (findRes !== E_API_ERROR_CODES.EC_OK) {
|
||||
throw new Error(`RG_FindEndPoints failed: ${errorToString(findRes)}`);
|
||||
}
|
||||
|
||||
const count = countPtr.deref();
|
||||
console.log('Найдено устройств:', count);
|
||||
if (count === 0) {
|
||||
throw new Error('USB HID не найден');
|
||||
}
|
||||
const endpointInfo = new RG_ENDPOINT_INFO();
|
||||
const infoRes = rgsec.RG_GetFoundEndPointInfo(
|
||||
endpointListPtr.deref(),
|
||||
0,
|
||||
endpointInfo.ref()
|
||||
);
|
||||
console.log('После RG_GetFoundEndPointInfo, код:', infoRes);
|
||||
|
||||
if (infoRes !== E_API_ERROR_CODES.EC_OK) {
|
||||
throw new Error(`RG_GetFoundEndPointInfo failed: ${errorToString(infoRes)}`);
|
||||
}
|
||||
|
||||
const endpoint = new RG_ENDPOINT();
|
||||
endpoint.type = endpointInfo.type;
|
||||
endpoint.address = readCString(
|
||||
endpointInfo.address.buffer,
|
||||
endpointInfo.address.length
|
||||
);
|
||||
console.log('Endpoint type:', endpoint.type);
|
||||
console.log('Endpoint address:', endpoint.address);
|
||||
|
||||
const address = 0;
|
||||
|
||||
const initDevRes = rgsec.RG_InitDevice(endpoint.ref(), address);
|
||||
console.log('После RG_InitDevice, код:', initDevRes);
|
||||
|
||||
if (initDevRes !== E_API_ERROR_CODES.EC_OK) {
|
||||
throw new Error(`RG_InitDevice failed: ${errorToString(initDevRes)}`);
|
||||
}
|
||||
|
||||
console.log('инициализация прошла успешно');
|
||||
const deviceInfoExt = new RG_DEVICE_INFO_EXT();
|
||||
const infoExtRes = rgsec.RG_GetInfoExt(
|
||||
endpoint.ref(),
|
||||
address,
|
||||
deviceInfoExt.ref()
|
||||
);
|
||||
console.log('После RG_GetInfoExt, код:', infoExtRes);
|
||||
|
||||
if (infoExtRes === E_API_ERROR_CODES.EC_OK) {
|
||||
console.log('номер:', deviceInfoExt.serial.toString(16));
|
||||
console.log('прошивка:', deviceInfoExt.firmware.toString(16));
|
||||
} else {
|
||||
console.warn('Не удалось получить расширенную информацию об устройстве');
|
||||
}
|
||||
|
||||
const queuePtr = ref.alloc('pointer');
|
||||
console.log('Подписка на события...');
|
||||
const subscribeRes = rgsec.RG_Subscribe(
|
||||
queuePtr,
|
||||
endpoint.ref(),
|
||||
address,
|
||||
E_RG_DEVICE_EVENT_TYPE.DET_CARD_PLACED_EVENT |
|
||||
E_RG_DEVICE_EVENT_TYPE.DET_CARD_REMOVED_EVENT,
|
||||
ref.NULL
|
||||
);
|
||||
console.log('После RG_Subscribe, код:', subscribeRes);
|
||||
|
||||
if (subscribeRes !== E_API_ERROR_CODES.EC_OK) {
|
||||
throw new Error(`RG_Subscribe failed: ${errorToString(subscribeRes)}`);
|
||||
}
|
||||
|
||||
const eventQueue = queuePtr.deref();
|
||||
console.log('Ожидание событий...');
|
||||
const eventTypePtr = ref.alloc('uint32');
|
||||
const eventDataPtrPtr = ref.alloc('pointer');
|
||||
|
||||
const pollEvents = () => {
|
||||
const pollRes = rgsec.RG_PollEvents(
|
||||
eventQueue,
|
||||
eventTypePtr,
|
||||
eventDataPtrPtr,
|
||||
1000
|
||||
);
|
||||
console.log('PollEvents, код:', pollRes);
|
||||
|
||||
switch (pollRes) {
|
||||
case E_API_ERROR_CODES.EC_OK:
|
||||
handleEvent(eventTypePtr.deref(), eventDataPtrPtr.deref());
|
||||
setImmediate(pollEvents);
|
||||
break;
|
||||
|
||||
case E_API_ERROR_CODES.EC_POLL_NO_EVENTS:
|
||||
setImmediate(pollEvents);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error('Ошибка PollEvents:', errorToString(pollRes));
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvent = (eventType, eventDataPtr) => {
|
||||
console.log('handleEvent, тип события:', eventType);
|
||||
switch (eventType) {
|
||||
case E_RG_DEVICE_EVENT_TYPE.DET_CARD_PLACED_EVENT:
|
||||
const cardInfo = ref.reinterpret(eventDataPtr, RG_CARD_INFO.size).deref();
|
||||
const uid = Array.from(cardInfo.uid).map(b => b.toString(16).padStart(2, '0'));
|
||||
console.log(`Карта обнаружена: ${uid.join(':')}`);
|
||||
// Логгируем в admin-server
|
||||
console.log('[DEBUG] Отправляю событие на admin-server:', { event, uid, timestamp });
|
||||
axios.post('http://localhost:3005/api/card-event', {
|
||||
event: 'CARD_PLACED',
|
||||
uid: uid.join(''),
|
||||
timestamp: Date.now()
|
||||
}).catch(err => {
|
||||
console.error('[ADMIN][CARD_LOG] Ошибка отправки:', err.message);
|
||||
});
|
||||
break;
|
||||
|
||||
case E_RG_DEVICE_EVENT_TYPE.DET_CARD_REMOVED_EVENT:
|
||||
console.log('Карта удалена');
|
||||
// Логгируем в admin-server
|
||||
console.log('[DEBUG] Отправляю событие на admin-server:', { event, uid, timestamp });
|
||||
axios.post('http://localhost:3005/api/card-event', {
|
||||
event: 'CARD_REMOVED',
|
||||
uid: null,
|
||||
timestamp: Date.now()
|
||||
}).catch(err => {
|
||||
console.error('[ADMIN][CARD_LOG] Ошибка отправки:', err.message);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Необработанное событие: ${eventType}`);
|
||||
}
|
||||
};
|
||||
pollEvents();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Ошибка:', err.message);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nЗавершение работы...');
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
console.log('Очистка ресурсов...');
|
||||
rgsec.RG_CloseResource(endpointListPtr?.deref());
|
||||
rgsec.RG_CloseDevice(endpoint?.ref(), address);
|
||||
rgsec.RG_Uninitialize();
|
||||
console.log('Ресурсы освобождены.');
|
||||
} catch (cleanupErr) {
|
||||
console.error('Ошибка при очистке:', cleanupErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,401 @@
|
||||
/* Общие стили */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Окно логина */
|
||||
.login-window {
|
||||
max-width: 400px;
|
||||
margin: 12% auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
}
|
||||
|
||||
.login-window h2 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 95%;
|
||||
padding: 10px;
|
||||
border: 1px solid #b9b9b9;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
button[type="button"] {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button[type="button"]:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Информация о сотруднике */
|
||||
.employee-info {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.employee-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.employee-photo {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.photo-placeholder {
|
||||
color: #999;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.employee-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.employee-details h2 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.employee-details p {
|
||||
margin: 5px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Секция расчётных листов */
|
||||
.payslip-section {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.payslip-section h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.period-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.period-selector label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.period-selector select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.period-selector select:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
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 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
/* Делаем кнопку назад компактнее */
|
||||
.back-button {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Гостевой режим */
|
||||
.guest-mode {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.guest-content h2 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Навигация по табам */
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Контент табов */
|
||||
.tab-content {
|
||||
min-height: 400px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.handbook-content,
|
||||
.union-content,
|
||||
.info-content {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
white-space: pre-line;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* Заголовки в контенте */
|
||||
.tab-content h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.tab-content p {
|
||||
margin-bottom: 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* --- Handbook Content Styles --- */
|
||||
.handbook-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 12px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
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 h3 {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
}
|
||||
.handbook-content em {
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.handbook-content h4 {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.handbook-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.handbook-table th,
|
||||
.handbook-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.handbook-table th {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.handbook-content table,
|
||||
.handbook-content th,
|
||||
.handbook-content td,
|
||||
.handbook-content tr {
|
||||
border: 1px solid #bdbdbd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.handbook-content th,
|
||||
.handbook-content td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.handbook-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
declare module 'textract' {
|
||||
interface TextractOptions {
|
||||
preserveLineBreaks?: boolean;
|
||||
preserveOnlyMultipleLineBreaks?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function fromBufferWithMime(
|
||||
mimeType: string,
|
||||
buffer: Buffer,
|
||||
options: TextractOptions,
|
||||
callback: (error: Error | null, text: string) => void
|
||||
): void;
|
||||
|
||||
export = {
|
||||
fromBufferWithMime
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user