admin_to_react
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user