admin_to_react

This commit is contained in:
2026-08-09 14:13:06 +03:00
commit 5d2fc5773c
163 changed files with 140326 additions and 0 deletions
+102
View File
@@ -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;
+241
View File
@@ -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;
+105
View File
@@ -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;
+45
View File
@@ -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;