diff --git a/admin-frontend/src/components/Editor.jsx b/admin-frontend/src/components/Editor.jsx
index 88d1143..9abb3ff 100644
--- a/admin-frontend/src/components/Editor.jsx
+++ b/admin-frontend/src/components/Editor.jsx
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react';
import 'quill/dist/quill.snow.css';
import '../App.css';
import DOMPurify from 'dompurify';
+import SlidesManager from './SlidesManager';
function Editor() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
@@ -15,7 +16,6 @@ function Editor() {
const [editContent, setEditContent] = useState('');
const [editPreviewContent, setEditPreviewContent] = useState('');
const [showConfirmModal, setShowConfirmModal] = useState(false);
- //const [csrfToken, setCsrfToken] = useState('');
const [notify, setNotify] = useState({ message: '', type: '', show: false });
const editorRef = useRef(null);
@@ -29,17 +29,6 @@ function Editor() {
setNotify({ message: '', type: '', show: false });
}, 2200);
}, []);
-
-
- // Fetch CSRF token
- //useEffect(() => {
- // fetch('/moders/csrf-token')
- // .then(res => res.json())
- // .then(data => {
- // setCsrfToken(data.csrfToken);
- //})
- //.catch(() => {});
- //}, []);
// Check session on mount
useEffect(() => {
@@ -55,9 +44,9 @@ function Editor() {
.catch(() => setIsLoggedIn(false));
}, []); // eslint-disable-line react-hooks/exhaustive-deps
-const authenticatedFetch = useCallback((url, options = {}) => {
- return fetch(url, options);
-}, []);
+ const authenticatedFetch = useCallback((url, options = {}) => {
+ return fetch(url, options);
+ }, []);
const loadBlocks = useCallback(async (tab) => {
try {
@@ -105,7 +94,7 @@ const authenticatedFetch = useCallback((url, options = {}) => {
const handleTabChange = (tab) => {
setActiveTab(tab);
- if (isLoggedIn) {
+ if (isLoggedIn && tab !== 'slides') {
loadBlocks(tab);
}
};
@@ -138,41 +127,35 @@ const authenticatedFetch = useCallback((url, options = {}) => {
};
const handleSaveBlock = async (e) => {
- e.preventDefault();
- const content = DOMPurify.sanitize(editContent.trim());
- if (!content || content === '
{
if (e.target === e.currentTarget) handleTryCloseEditModal();
diff --git a/admin-frontend/src/components/SlidesManager.jsx b/admin-frontend/src/components/SlidesManager.jsx
new file mode 100644
index 0000000..9005e71
--- /dev/null
+++ b/admin-frontend/src/components/SlidesManager.jsx
@@ -0,0 +1,395 @@
+// src/components/SlidesManager.jsx
+import React, { useState, useEffect, useCallback } from 'react';
+import DOMPurify from 'dompurify';
+
+function SlidesManager() {
+ const [slides, setSlides] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [editingSlide, setEditingSlide] = useState(null);
+ const [formData, setFormData] = useState({
+ image_url: '',
+ title: '',
+ description: '',
+ link: '',
+ order: 0,
+ is_active: 1
+ });
+ const [uploading, setUploading] = useState(false);
+ const [notify, setNotify] = useState({ message: '', type: '', show: false });
+
+ const showNotify = useCallback((message, type = 'success') => {
+ setNotify({ message, type, show: true });
+ setTimeout(() => {
+ setNotify({ message: '', type: '', show: false });
+ }, 2500);
+ }, []);
+
+ // Загрузка слайдов
+ const loadSlides = useCallback(async () => {
+ setLoading(true);
+ try {
+ const res = await fetch('/moders/content/slides/all', {
+ credentials: 'include'
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setSlides(Array.isArray(data) ? data : []);
+ } else {
+ showNotify('Ошибка загрузки слайдов', 'error');
+ }
+ } catch (e) {
+ showNotify('Ошибка загрузки слайдов', 'error');
+ } finally {
+ setLoading(false);
+ }
+ }, [showNotify]);
+
+ useEffect(() => {
+ loadSlides();
+ }, [loadSlides]);
+
+ // Открыть модалку для добавления
+ const handleAddSlide = () => {
+ setEditingSlide(null);
+ setFormData({
+ image_url: '',
+ title: '',
+ description: '',
+ link: '',
+ order: slides.length,
+ is_active: 1
+ });
+ setShowEditModal(true);
+ };
+
+ // Открыть модалку для редактирования
+ const handleEditSlide = (slide) => {
+ setEditingSlide(slide);
+ setFormData({
+ image_url: slide.image_url || '',
+ title: slide.title || '',
+ description: slide.description || '',
+ link: slide.link || '',
+ order: slide.order || 0,
+ is_active: slide.is_active !== undefined ? slide.is_active : 1
+ });
+ setShowEditModal(true);
+ };
+
+ // Сохранить слайд
+ // В функции handleSaveSlide
+const handleSaveSlide = async (e) => {
+ e.preventDefault();
+
+ // ✅ ЛОГИ ДЛЯ ОТЛАДКИ
+ console.log('========================================');
+ console.log('📤 ОТПРАВКА ДАННЫХ НА СЕРВЕР');
+ console.log('📤 editingSlide:', editingSlide);
+ console.log('📤 formData:', JSON.stringify(formData, null, 2));
+ console.log('========================================');
+
+ if (!formData.image_url) {
+ showNotify('URL изображения обязателен', 'error');
+ return;
+ }
+
+ const url = editingSlide
+ ? `/moders/content/slides/${editingSlide.id}`
+ : '/moders/content/slides';
+
+ const method = editingSlide ? 'PUT' : 'POST';
+
+ try {
+ const res = await fetch(url, {
+ method,
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify(formData)
+ });
+
+ const data = await res.json();
+ console.log('📥 ОТВЕТ СЕРВЕРА:', res.status, data);
+
+ if (res.ok) {
+ showNotify(editingSlide ? 'Слайд обновлён' : 'Слайд добавлен', 'success');
+ setShowEditModal(false);
+ loadSlides();
+ } else {
+ showNotify(data.message || `Ошибка: ${res.status}`, 'error');
+ }
+ } catch (e) {
+ console.error('❌ Ошибка:', e);
+ showNotify('Ошибка сохранения', 'error');
+ }
+};
+
+ // Удалить слайд
+ const handleDeleteSlide = async (id) => {
+ if (!window.confirm('Удалить этот слайд?')) return;
+
+ try {
+ const res = await fetch(`/moders/content/slides/${id}`, {
+ method: 'DELETE',
+ credentials: 'include'
+ });
+ if (res.ok) {
+ showNotify('Слайд удалён', 'success');
+ loadSlides();
+ } else {
+ showNotify('Ошибка удаления', 'error');
+ }
+ } catch (e) {
+ showNotify('Ошибка удаления', 'error');
+ }
+ };
+
+ // Загрузка изображения
+ const handleUploadImage = async (file) => {
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('image', file);
+
+ setUploading(true);
+ try {
+ const res = await fetch('/moders/content/slides/upload', {
+ method: 'POST',
+ credentials: 'include',
+ body: formData
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setFormData(prev => ({ ...prev, image_url: data.path }));
+ showNotify('Изображение загружено', 'success');
+ } else {
+ showNotify('Ошибка загрузки изображения', 'error');
+ }
+ } catch (e) {
+ showNotify('Ошибка загрузки изображения', 'error');
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ return (
+
+
+
📸 Слайды карусели
+
+
+
+ {/* Уведомления */}
+ {notify.show && (
+
+ {notify.message}
+
+ )}
+
+ {/* Список слайдов */}
+ {loading ? (
+
Загрузка...
+ ) : slides.length === 0 ? (
+
Нет слайдов. Добавьте первый!
+ ) : (
+
+ {slides.map(slide => (
+
+

{
+ e.target.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="100%25" height="100%25"%3E%3Crect width="100%25" height="100%25" fill="%23ddd"/%3E%3Ctext x="50%25" y="50%25" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3E%D0%9D%D0%B5%D1%82%20%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F%3C/text%3E%3C/svg%3E';
+ }}
+ />
+
+
{slide.title || 'Без названия'}
+
+ {slide.description || ''}
+
+
+ Порядок: {slide.order}
+ •
+
+ {slide.is_active ? 'Активен' : 'Неактивен'}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Модалка редактирования */}
+ {showEditModal && (
+
{
+ if (e.target === e.currentTarget) setShowEditModal(false);
+ }}>
+
+
+
+
+ {editingSlide ? 'Редактировать слайд' : 'Добавить слайд'}
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default SlidesManager;
\ No newline at end of file
diff --git a/admin-server1/logs/admin.log b/admin-server1/logs/admin.log
index 760fa64..35107ba 100644
--- a/admin-server1/logs/admin.log
+++ b/admin-server1/logs/admin.log
@@ -2017,3 +2017,322 @@
[2026-08-18T13:21:48.177Z] [INFO] Login successful
[2026-08-18T13:21:48.283Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
[2026-08-18T13:21:48.296Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
+[2026-08-19T06:49:14.829Z] [INFO] Server running on http://localhost:3005
+[2026-08-19T06:49:14.837Z] [INFO] Card logs initialized successfully
+[2026-08-19T08:16:23.702Z] [INFO] Server running on http://localhost:3005
+[2026-08-19T08:16:23.706Z] [INFO] Card logs initialized successfully
+[2026-08-19T09:32:26.775Z] [INFO] Server running on http://localhost:3005
+[2026-08-19T09:32:26.778Z] [INFO] Card logs initialized successfully
+[2026-08-19T09:54:13.538Z] [INFO] Server running on http://localhost:3005
+[2026-08-19T09:54:13.541Z] [INFO] Card logs initialized successfully
+[2026-08-19T10:03:57.687Z] [INFO] Server running on http://localhost:3005
+[2026-08-19T10:03:57.691Z] [INFO] Card logs initialized successfully
+[2026-08-19T13:16:59.418Z] [INFO] Login successful
+[2026-08-19T13:16:59.575Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
+[2026-08-19T13:16:59.591Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
+[2026-08-19T13:17:34.211Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-19T13:17:34.212Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-19T13:17:34.217Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-19T13:17:34.217Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-19T13:17:34.291Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-19T13:17:34.292Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-19T13:17:34.293Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-22T10:02:00.127Z] [INFO] Server running on http://localhost:3005
+[2026-08-22T10:02:00.130Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:16:54.329Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:16:54.345Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:17:44.305Z] [WARN] Неверный пароль
+[2026-08-28T13:17:47.842Z] [WARN] Неверный пароль
+[2026-08-28T13:17:49.122Z] [WARN] Неверный пароль
+[2026-08-28T13:18:24.580Z] [INFO] Login successful
+[2026-08-28T13:18:24.735Z] [INFO] [DB CHECK] Проверка подключения к MySQL: localhost:3306/rasch_content
+[2026-08-28T13:18:24.756Z] [INFO] [DB CHECK] ✅ Успешное подключение к MySQL
+[2026-08-28T13:18:42.365Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:18:42.366Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:18:42.380Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:18:42.381Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:18:42.462Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:18:42.463Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:18:42.464Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T13:21:07.055Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:21:07.058Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:21:24.349Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart.
(D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:21:28.269Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:22:13.764Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:25:47.640Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:25:47.643Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:25:57.085Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:28:40.679Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:35:09.914Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:36:00.169Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:36:00.172Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:37:58.077Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:37:58.080Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:38:24.214Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:38:54.054Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:38:57.735Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:40:58.165Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:40:58.168Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:41:09.217Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:41:09.218Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:41:09.220Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:41:09.220Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:41:09.298Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:41:09.299Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:41:09.301Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T13:41:18.242Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:44:33.109Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:44:33.112Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:44:45.327Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:47:57.993Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:47:57.997Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:52:17.446Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:52:17.449Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:53:18.856Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:53:18.859Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:53:30.533Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:53:30.534Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:53:30.546Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:53:30.547Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:53:30.646Z] [INFO] [LOGIN] Результат сравнения пароля: false
+[2026-08-28T13:53:30.646Z] [WARN] [LOGIN] Неверный пароль для пользователя: vova
+[2026-08-28T13:53:36.962Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:53:36.963Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:53:36.965Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:53:36.965Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:53:37.043Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:53:37.043Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:53:37.045Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T13:54:12.715Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:54:12.715Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:54:12.717Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:54:12.718Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:54:12.803Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:54:12.804Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:54:12.805Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T13:54:29.593Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:55:32.725Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:55:32.728Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:55:44.526Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T13:57:30.453Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:57:30.456Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:57:37.890Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:57:37.891Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:57:37.893Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:57:37.893Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:57:37.973Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:57:37.973Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:57:37.975Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T13:59:04.163Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T13:59:04.166Z] [INFO] Card logs initialized successfully
+[2026-08-28T13:59:18.113Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T13:59:18.113Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T13:59:18.128Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T13:59:18.129Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T13:59:18.259Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T13:59:18.260Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T13:59:18.262Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T14:02:13.224Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T14:02:13.226Z] [INFO] Card logs initialized successfully
+[2026-08-28T14:02:25.923Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T14:02:25.924Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T14:02:25.940Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T14:02:25.941Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T14:02:26.078Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T14:02:26.087Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T14:02:26.089Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T14:03:37.800Z] [ERROR] MulterError: Unexpected field
+ at wrappedFileFilter (D:\rasch_git\electron\admin-server1\node_modules\multer\index.js:41:19)
+ at Multipart. (D:\rasch_git\electron\admin-server1\node_modules\multer\lib\make-middleware.js:193:7)
+ at Multipart.emit (node:events:509:28)
+ at HeaderParser.cb (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:358:14)
+ at HeaderParser.push (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:162:20)
+ at SBMH.ssCb [as _cb] (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:394:37)
+ at feed (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:219:14)
+ at SBMH.push (D:\rasch_git\electron\admin-server1\node_modules\streamsearch\lib\sbmh.js:104:16)
+ at Multipart._write (D:\rasch_git\electron\admin-server1\node_modules\busboy\lib\types\multipart.js:567:19)
+ at writeOrBuffer (node:internal/streams/writable:570:12)
+[2026-08-28T14:05:00.439Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T14:05:00.442Z] [INFO] Card logs initialized successfully
+[2026-08-28T14:05:47.706Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T14:05:47.707Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T14:05:47.709Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T14:05:47.710Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T14:05:47.796Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T14:05:47.797Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T14:05:47.798Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T14:06:31.202Z] [INFO] [LOGIN] Попытка входа: grivova
+[2026-08-28T14:06:31.202Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T14:06:31.204Z] [INFO] [LOGIN] Результат поиска пользователя: не найден
+[2026-08-28T14:06:31.204Z] [WARN] [LOGIN] Пользователь не найден: grivova
+[2026-08-28T14:06:43.404Z] [INFO] [LOGIN] Попытка входа: asutp
+[2026-08-28T14:06:43.406Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T14:06:43.407Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T14:06:43.408Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T14:06:43.489Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T14:06:43.490Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":5,"username":"asutp"}
+[2026-08-28T14:06:43.491Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T14:07:57.016Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T14:07:57.018Z] [INFO] Card logs initialized successfully
+[2026-08-28T14:08:15.541Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-28T14:08:15.543Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-28T14:08:15.546Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-28T14:08:15.547Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-28T14:08:15.629Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-28T14:08:15.630Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-28T14:08:15.631Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-28T14:09:51.626Z] [INFO] Server running on http://localhost:3005
+[2026-08-28T14:09:51.629Z] [INFO] Card logs initialized successfully
+[2026-08-31T05:58:23.110Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T05:58:23.117Z] [INFO] Card logs initialized successfully
+[2026-08-31T11:41:26.146Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T11:41:26.163Z] [INFO] Card logs initialized successfully
+[2026-08-31T11:41:58.530Z] [INFO] [LOGIN] Попытка входа: vova
+[2026-08-31T11:41:58.531Z] [INFO] [LOGIN] Проверка подключения к базе...
+[2026-08-31T11:41:58.554Z] [INFO] [LOGIN] Результат поиска пользователя: найден
+[2026-08-31T11:41:58.562Z] [INFO] [LOGIN] Сравнение пароля через bcrypt...
+[2026-08-31T11:41:58.657Z] [INFO] [LOGIN] Результат сравнения пароля: true
+[2026-08-31T11:41:58.661Z] [INFO] [LOGIN] Сессия установлена: {"isModerator":true,"userId":4,"username":"vova"}
+[2026-08-31T11:41:58.668Z] [INFO] [LOGIN] Ответ отправлен
+[2026-08-31T12:06:00.544Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:06:00.547Z] [INFO] Card logs initialized successfully
+[2026-08-31T12:18:40.420Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:18:40.423Z] [INFO] Card logs initialized successfully
+[2026-08-31T12:21:29.695Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:21:29.697Z] [INFO] Card logs initialized successfully
+[2026-08-31T12:23:29.133Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:23:29.137Z] [INFO] Card logs initialized successfully
+[2026-08-31T12:27:21.641Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:27:21.644Z] [INFO] Card logs initialized successfully
+[2026-08-31T12:30:33.180Z] [INFO] Server running on http://localhost:3005
+[2026-08-31T12:30:33.183Z] [INFO] Card logs initialized successfully
diff --git a/admin-server1/moders/routes/content.js b/admin-server1/moders/routes/content.js
index b94e107..ed6e719 100644
--- a/admin-server1/moders/routes/content.js
+++ b/admin-server1/moders/routes/content.js
@@ -9,93 +9,211 @@ const fs = require('fs');
const uploadDir = path.join(__dirname, '../../uploads');
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
const storage = multer.diskStorage({
- destination: (req, file, cb) => cb(null, uploadDir),
- filename: (req, file, cb) => {
- const ext = path.extname(file.originalname);
- const name = Date.now() + '-' + Math.round(Math.random() * 1e6) + ext;
- cb(null, name);
- }
+ destination: (req, file, cb) => cb(null, uploadDir),
+ filename: (req, file, cb) => {
+ const ext = path.extname(file.originalname);
+ const name = Date.now() + '-' + Math.round(Math.random() * 1e6) + ext;
+ cb(null, name);
+ }
});
const ALLOWED_MIME_TYPES = [
- 'image/jpeg', 'image/png',
- 'video/mp4', 'video/quicktime'
+ 'image/jpeg', 'image/png',
+ 'video/mp4', 'video/quicktime'
];
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 МБ
const upload = multer({
- storage,
- limits: { fileSize: MAX_FILE_SIZE },
- fileFilter: (req, file, cb) => {
- if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {
- cb(null, true);
- } else {
- cb(new Error('Недопустимый тип файла'));
+ storage,
+ limits: { fileSize: MAX_FILE_SIZE },
+ fileFilter: (req, file, cb) => {
+ if (ALLOWED_MIME_TYPES.includes(file.mimetype)) {
+ cb(null, true);
+ } else {
+ cb(new Error('Недопустимый тип файла'));
+ }
}
- }
});
+// ============================================
+// 📸 СЛАЙДЫ ДЛЯ КАРУСЕЛИ (В САМОМ НАЧАЛЕ)
+// ============================================
+
+// Получить все активные слайды
+router.get('/slides', async (req, res) => {
+ console.log('📸 GET /slides');
+ try {
+ const [rows] = await db.query(
+ 'SELECT * FROM slides WHERE is_active = 1 ORDER BY `order` ASC'
+ );
+ res.json(rows);
+ } catch (err) {
+ console.error('❌ Ошибка получения слайдов:', err);
+ res.status(500).json({ message: 'Ошибка сервера', error: err.message });
+ }
+});
+
+// Получить все слайды (для админки)
+router.get('/slides/all', async (req, res) => {
+ console.log('📸 GET /slides/all');
+ try {
+ const [rows] = await db.query(
+ 'SELECT * FROM slides ORDER BY `order` ASC'
+ );
+ res.json(rows);
+ } catch (err) {
+ console.error('❌ Ошибка получения всех слайдов:', err);
+ res.status(500).json({ message: 'Ошибка сервера', error: err.message });
+ }
+});
+
+// Загрузка изображения для слайда
+router.post('/slides/upload', upload.single('image'), (req, res) => {
+ console.log('📸 POST /slides/upload');
+ console.log('📁 req.file:', req.file);
+
+ if (!req.file) {
+ return res.status(400).json({ message: 'Файл не загружен' });
+ }
+ const relPath = '/uploads/' + req.file.filename;
+ console.log('✅ Изображение загружено:', relPath);
+ res.json({ path: relPath });
+});
+
+// Создать слайд
+router.post('/slides', async (req, res) => {
+ console.log('📸 POST /slides');
+ const { image_url, title, description, link, order, is_active } = req.body;
+
+ if (!image_url) {
+ return res.status(400).json({ message: 'image_url обязателен' });
+ }
+
+ try {
+ const [result] = await db.query(
+ `INSERT INTO slides (image_url, title, description, link, \`order\`, is_active, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW())`,
+ [image_url, title || null, description || null, link || null, order || 0, is_active !== undefined ? is_active : 1]
+ );
+ console.log('✅ Слайд создан, ID:', result.insertId);
+ res.json({ id: result.insertId, message: 'Слайд создан' });
+ } catch (err) {
+ console.error('❌ Ошибка создания слайда:', err);
+ res.status(500).json({ message: 'Ошибка сервера', error: err.message });
+ }
+});
+
+// Обновить слайд
+router.put('/slides/:id', async (req, res) => {
+ console.log('📸 PUT /slides/' + req.params.id);
+ const { id } = req.params;
+ const { image_url, title, description, link, order, is_active } = req.body;
+
+ if (!image_url) {
+ return res.status(400).json({ message: 'image_url обязателен' });
+ }
+
+ try {
+ await db.query(
+ `UPDATE slides
+ SET image_url = ?,
+ title = ?,
+ description = ?,
+ link = ?,
+ \`order\` = ?,
+ is_active = ?,
+ updated_at = NOW()
+ WHERE id = ?`,
+ [image_url, title || null, description || null, link || null, order || 0, is_active !== undefined ? is_active : 1, id]
+ );
+ console.log('✅ Слайд обновлён, ID:', id);
+ res.json({ message: 'Слайд обновлён' });
+ } catch (err) {
+ console.error('❌ Ошибка обновления слайда:', err);
+ res.status(500).json({ message: 'Ошибка сервера', error: err.message });
+ }
+});
+
+// Удалить слайд
+router.delete('/slides/:id', async (req, res) => {
+ console.log('📸 DELETE /slides/' + req.params.id);
+ const { id } = req.params;
+ try {
+ await db.query('DELETE FROM slides WHERE id = ?', [id]);
+ console.log('✅ Слайд удалён, ID:', id);
+ res.json({ message: 'Слайд удалён' });
+ } catch (err) {
+ console.error('❌ Ошибка удаления слайда:', err);
+ res.status(500).json({ message: 'Ошибка сервера', error: err.message });
+ }
+});
+
+// ============================================
+// 📚 ОСТАЛЬНЫЕ МАРШРУТЫ (НИЖЕ)
+// ============================================
+
// Получить все блоки для вкладки
router.get('/:tab', async (req, res) => {
- const { tab } = req.params;
- if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
- try {
- const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]);
- res.json(rows);
- } catch (err) {
- res.status(500).json({ message: 'Ошибка сервера' });
- }
+ const { tab } = req.params;
+ if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
+ try {
+ const [rows] = await db.query('SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC', [tab]);
+ res.json(rows);
+ } catch (err) {
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
});
// Загрузка файла (image/video)
router.post('/:tab/upload', requireModerator, upload.single('file'), (req, res) => {
- if (!req.file) return res.status(400).json({ message: 'Файл не загружен' });
- const relPath = '/uploads/' + req.file.filename;
- res.json({ path: relPath });
+ if (!req.file) return res.status(400).json({ message: 'Файл не загружен' });
+ const relPath = '/uploads/' + req.file.filename;
+ res.json({ path: relPath });
});
// Добавить новый блок
router.post('/:tab', requireModerator, async (req, res) => {
- const { tab } = req.params;
- const { content } = req.body;
- if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
- try {
- const [result] = await db.query(
- 'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
- [tab, 'text', content, req.session.userId]
- );
- res.json({ id: result.insertId });
- } catch (err) {
- res.status(500).json({ message: 'Ошибка сервера' });
- }
+ const { tab } = req.params;
+ const { content } = req.body;
+ if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
+ try {
+ const [result] = await db.query(
+ 'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
+ [tab, 'text', content, req.session.userId]
+ );
+ res.json({ id: result.insertId });
+ } catch (err) {
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
});
// Обновить блок
router.put('/:tab/:id', requireModerator, async (req, res) => {
- const { tab, id } = req.params;
- const { content } = req.body;
- if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
- try {
- await db.query(
- 'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?',
- [content, id, tab]
- );
- res.json({ message: 'Обновлено' });
- } catch (err) {
- res.status(500).json({ message: 'Ошибка сервера' });
- }
+ const { tab, id } = req.params;
+ const { content } = req.body;
+ if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
+ try {
+ await db.query(
+ 'UPDATE content_blocks SET content = ?, updated_at = NOW() WHERE id = ? AND tab = ?',
+ [content, id, tab]
+ );
+ res.json({ message: 'Обновлено' });
+ } catch (err) {
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
});
// Удалить блок
router.delete('/:tab/:id', requireModerator, async (req, res) => {
- const { tab, id } = req.params;
- if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
- try {
- await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]);
- res.json({ message: 'Удалено' });
- } catch (err) {
- res.status(500).json({ message: 'Ошибка сервера' });
- }
+ const { tab, id } = req.params;
+ if (!['info', 'union', 'events', 'help', 'housing'].includes(tab)) return res.status(400).json({ message: 'Некорректная вкладка' });
+ try {
+ await db.query('DELETE FROM content_blocks WHERE id = ? AND tab = ?', [id, tab]);
+ res.json({ message: 'Удалено' });
+ } catch (err) {
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
});
+
router.get('/section/:section', async (req, res) => {
const { section } = req.params;
if (!['events', 'help', 'housing'].includes(section)) {
@@ -106,7 +224,6 @@ router.get('/section/:section', async (req, res) => {
'SELECT * FROM content_blocks WHERE tab = ? ORDER BY created_at ASC',
[section]
);
- // Если есть блоки — возвращаем их, иначе пустой массив
res.json(rows);
} catch (err) {
console.error(err);
@@ -114,7 +231,7 @@ router.get('/section/:section', async (req, res) => {
}
});
-// Сохранить контент раздела (очищаем старые и вставляем новый)
+// Сохранить контент раздела
router.post('/section/:section', requireModerator, async (req, res) => {
const { section } = req.params;
const { content } = req.body;
@@ -124,10 +241,8 @@ router.post('/section/:section', requireModerator, async (req, res) => {
}
try {
- // Удаляем старые блоки для этого раздела
await db.query('DELETE FROM content_blocks WHERE tab = ?', [section]);
- // Вставляем новый блок
const [result] = await db.query(
'INSERT INTO content_blocks (tab, type, content, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
[section, 'text', content, req.session.userId]
@@ -143,4 +258,5 @@ router.post('/section/:section', requireModerator, async (req, res) => {
res.status(500).json({ message: 'Ошибка сервера' });
}
});
-module.exports = router;
\ No newline at end of file
+
+module.exports = router;
\ No newline at end of file
diff --git a/admin-server1/uploads/1787925962367-682893.png b/admin-server1/uploads/1787925962367-682893.png
new file mode 100644
index 0000000..74d6f52
Binary files /dev/null and b/admin-server1/uploads/1787925962367-682893.png differ
diff --git a/admin-server1/uploads/1787926013867-685930.png b/admin-server1/uploads/1787926013867-685930.png
new file mode 100644
index 0000000..74d6f52
Binary files /dev/null and b/admin-server1/uploads/1787926013867-685930.png differ
diff --git a/admin-server1/uploads/1788176541470-547073.png b/admin-server1/uploads/1788176541470-547073.png
new file mode 100644
index 0000000..d234099
Binary files /dev/null and b/admin-server1/uploads/1788176541470-547073.png differ
diff --git a/admin-server1/uploads/1788183146109-428535.png b/admin-server1/uploads/1788183146109-428535.png
new file mode 100644
index 0000000..bf77ed9
Binary files /dev/null and b/admin-server1/uploads/1788183146109-428535.png differ
diff --git a/admin-server1/uploads/1788184329751-840142.png b/admin-server1/uploads/1788184329751-840142.png
new file mode 100644
index 0000000..74b1194
Binary files /dev/null and b/admin-server1/uploads/1788184329751-840142.png differ
diff --git a/admin-server1/uploads/1788184340940-777129.png b/admin-server1/uploads/1788184340940-777129.png
new file mode 100644
index 0000000..74b1194
Binary files /dev/null and b/admin-server1/uploads/1788184340940-777129.png differ
diff --git a/admin-server1/uploads/1788184553043-530019.png b/admin-server1/uploads/1788184553043-530019.png
new file mode 100644
index 0000000..d234099
Binary files /dev/null and b/admin-server1/uploads/1788184553043-530019.png differ
diff --git a/backend/config.json b/backend/config.json
index 06c647b..be7bd5c 100644
--- a/backend/config.json
+++ b/backend/config.json
@@ -1,5 +1,10 @@
{
- "payslipsPath": "\\\\PC-20010\\payslips2",
+ "payslipsPath": "./payslips",
+ "networkCredentials": {
+ "username": "",
+ "password": "",
+ "domain": "WORKGROUP"
+ },
"server": {
"host": "localhost",
"port": "3001"
@@ -51,9 +56,9 @@
}
],
"payslips": [
- "Петров_ПП_2026-08.pdf",
- "Сидорова_АС_2026-08.pdf",
- "Иванов_ИИ_2026-08.pdf"
+ "001.pdf",
+ "002.pdf",
+ "003.pdf"
]
}
}
\ No newline at end of file
diff --git a/backend/payslips/022026/002.pdf b/backend/payslips/022026/002.pdf
new file mode 100644
index 0000000..2eed526
Binary files /dev/null and b/backend/payslips/022026/002.pdf differ
diff --git a/backend/payslips/042026/003.pdf b/backend/payslips/042026/003.pdf
new file mode 100644
index 0000000..43de28f
Binary files /dev/null and b/backend/payslips/042026/003.pdf differ
diff --git a/backend/payslips/112025/001.pdf b/backend/payslips/112025/001.pdf
new file mode 100644
index 0000000..88c7043
Binary files /dev/null and b/backend/payslips/112025/001.pdf differ
diff --git a/backend/payslips/112025/001.xlsx b/backend/payslips/112025/001.xlsx
new file mode 100644
index 0000000..f50ec53
Binary files /dev/null and b/backend/payslips/112025/001.xlsx differ
diff --git a/backend/src/routes/payslipForm.js b/backend/src/routes/payslipForm.js
new file mode 100644
index 0000000..73ff135
--- /dev/null
+++ b/backend/src/routes/payslipForm.js
@@ -0,0 +1,117 @@
+const express = require('express');
+const path = require('path');
+const fs = require('fs').promises;
+const { logger } = require('../config/logger');
+const { parsePayslipExcel } = require('../services/excelParser');
+const { generatePayslipForm } = require('../services/formGenerator');
+
+const router = express.Router();
+
+/**
+ * GET /api/payslip/form/:tnom
+ * Получить HTML-форму расчетного листка
+ */
+router.get('/form/:tnom', async (req, res) => {
+ try {
+ const { tnom } = req.params;
+ const { period } = req.query;
+
+ logger.info(`[PayslipForm] Запрос формы для ${tnom}, период: ${period || 'не указан'}`);
+
+ // 1. Находим Excel-файл
+ const configPath = path.join(__dirname, '../../config.json');
+ const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
+
+ let payslipBasePath = config.payslipsPath || './payslips';
+ if (!path.isAbsolute(payslipBasePath)) {
+ payslipBasePath = path.join(__dirname, '../..', payslipBasePath);
+ }
+
+ let excelPath;
+ if (period) {
+ excelPath = path.join(payslipBasePath, period, `${tnom}.xlsx`);
+ } else {
+ excelPath = path.join(payslipBasePath, `${tnom}.xlsx`);
+ }
+
+ logger.info(`[PayslipForm] Путь к Excel: ${excelPath}`);
+
+ // 2. Проверяем существование файла
+ try {
+ await fs.access(excelPath);
+ } catch (err) {
+ logger.warn(`[PayslipForm] Excel не найден: ${excelPath}`);
+ return res.status(404).json({
+ message: 'Excel-файл расчетного листка не найден',
+ path: excelPath
+ });
+ }
+
+ // 3. Парсим Excel
+ const data = parsePayslipExcel(excelPath);
+ logger.info(`[PayslipForm] Данные извлечены:`, {
+ fullName: data.fullName,
+ tabNumber: data.tabNumber,
+ totalAccrued: data.totalAccrued,
+ totalPaid: data.totalPaid
+ });
+
+ // 4. Генерируем HTML-форму
+ const html = generatePayslipForm(data);
+
+ // 5. Отправляем HTML
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.send(html);
+
+ } catch (error) {
+ logger.error('[PayslipForm] Ошибка:', error.message);
+ res.status(500).json({
+ message: 'Ошибка при формировании расчетного листка',
+ error: error.message
+ });
+ }
+});
+
+/**
+ * GET /api/payslip/form-data/:tnom
+ * Получить данные из Excel в JSON (для отладки)
+ */
+router.get('/form-data/:tnom', async (req, res) => {
+ try {
+ const { tnom } = req.params;
+ const { period } = req.query;
+
+ // Находим Excel-файл
+ const configPath = path.join(__dirname, '../../config.json');
+ const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
+
+ let payslipBasePath = config.payslipsPath || './payslips';
+ if (!path.isAbsolute(payslipBasePath)) {
+ payslipBasePath = path.join(__dirname, '../..', payslipBasePath);
+ }
+
+ let excelPath;
+ if (period) {
+ excelPath = path.join(payslipBasePath, period, `${tnom}.xlsx`);
+ } else {
+ excelPath = path.join(payslipBasePath, `${tnom}.xlsx`);
+ }
+
+ // Проверяем существование
+ try {
+ await fs.access(excelPath);
+ } catch (err) {
+ return res.status(404).json({ message: 'Excel не найден' });
+ }
+
+ // Парсим и возвращаем JSON
+ const data = parsePayslipExcel(excelPath);
+ res.json(data);
+
+ } catch (error) {
+ logger.error('[PayslipForm] Ошибка получения данных:', error.message);
+ res.status(500).json({ error: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/src/server.js b/backend/src/server.js
index f873f4f..b4a3662 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -27,10 +27,11 @@ async function readJson(filePath) {
}
async function pathExists(filePath) {
+ console.log('🔍 Проверка пути:', filePath);
try {
await fsPromises.access(filePath);
return true;
- } catch {
+ } catch (err) {
return false;
}
}
@@ -69,7 +70,9 @@ async function connectToNetworkShare(config) {
// ============================================
const app = express();
+
app.use(cors());
+
app.use(express.json());
app.use(function(req, res, next) {
@@ -81,7 +84,31 @@ app.use(function(req, res, next) {
next();
});
-app.use(helmet());
+// ✅ ИСПРАВЛЕННАЯ НАСТРОЙКА HELMET (разрешаем iframe)
+app.use(helmet({
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ // РАЗРЕШАЕМ встраивание в iframe с localhost:3000
+ frameAncestors: ["'self'", "http://localhost:3000", "http://127.0.0.1:3000"],
+ // Для PDF файлов
+ frameSrc: ["'self'", "blob:"],
+ // Другие необходимые директивы
+ scriptSrc: ["'self'", "'unsafe-inline'"],
+ styleSrc: ["'self'", "'unsafe-inline'"],
+ imgSrc: ["'self'", "data:", "blob:"],
+ connectSrc: ["'self'"],
+ fontSrc: ["'self'"],
+ objectSrc: ["'self'", "blob:"],
+ mediaSrc: ["'self'"],
+ },
+ },
+ // Отключаем лишние политики для разработки
+ crossOriginEmbedderPolicy: false,
+ crossOriginOpenerPolicy: false,
+ crossOriginResourcePolicy: { policy: "cross-origin" },
+}));
+
app.use('/api/', limiter);
// ============================================
@@ -342,6 +369,269 @@ app.post('/api/rates/update-from-nbrb', async function(req, res) {
}
});
+// ============================================
+// 📄 РАСЧЕТНЫЕ ЛИСТКИ (ОБНОВЛЕННЫЕ ЭНДПОИНТЫ)
+// ============================================
+
+// ✅ НОВАЯ ФУНКЦИЯ: получает путь к файлу с учетом периода (месяца)
+async function getPayslipFilePath(tnom, period = null) {
+ const configPath = path.join(__dirname, '../config.json');
+ const config = await readJson(configPath);
+ let payslipBasePath = config.payslipsPath;
+
+ if (!path.isAbsolute(payslipBasePath)) {
+ payslipBasePath = path.join(__dirname, '..', payslipBasePath);
+ }
+
+ let filePath;
+ if (period) {
+ // ✅ Если передан период — ищем в подпапке
+ filePath = path.join(payslipBasePath, period, tnom + '.pdf');
+ console.log(`📁 Поиск в подпапке: ${period} -> ${filePath}`);
+ } else {
+ // ✅ Если период не передан — ищем в корне (для совместимости)
+ filePath = path.join(payslipBasePath, tnom + '.pdf');
+ console.log(`📁 Поиск в корне: ${filePath}`);
+ }
+
+ return filePath;
+}
+
+// ✅ ОБНОВЛЕННАЯ ПРОВЕРКА наличия расчетного листка (с period)
+app.get('/api/payslip/check/:tnom', async function(req, res) {
+ try {
+ var tnom = req.params.tnom;
+ var period = req.query.period || null; // ✅ Берем period из query-параметров
+
+ console.log('==========================================');
+ console.log(`📁 [PAYSLIP CHECK] Табельный номер: ${tnom}, Период: ${period || 'не указан'}`);
+
+ const filePath = await getPayslipFilePath(tnom, period);
+ const exists = await pathExists(filePath);
+
+ console.log(`📁 exists: ${exists}`);
+ console.log('==========================================');
+
+ res.json({
+ exists: exists,
+ tnom: tnom,
+ period: period,
+ path: filePath,
+ fileName: tnom + '.pdf'
+ });
+
+ } catch (err) {
+ logger.error('Ошибка проверки расчетного листка:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// ✅ ОБНОВЛЕННОЕ ПОЛУЧЕНИЕ URL (с period)
+app.get('/api/payslip/url/:tnom', async function(req, res) {
+ try {
+ var tnom = req.params.tnom;
+ var period = req.query.period || null; // ✅ Берем period из query-параметров
+
+ console.log('==========================================');
+ console.log(`📁 [PAYSLIP URL] Табельный номер: ${tnom}, Период: ${period || 'не указан'}`);
+
+ const filePath = await getPayslipFilePath(tnom, period);
+ console.log(`📁 Полный путь к файлу: ${filePath}`);
+
+ const exists = await pathExists(filePath);
+ console.log(`📁 exists: ${exists}`);
+ console.log('==========================================');
+
+ if (exists) {
+ // ✅ Формируем URL для получения файла
+ let fileUrl = `/api/payslip/tnom/${tnom}`;
+ if (period) {
+ fileUrl += `?period=${period}`; // ✅ Добавляем period в query
+ }
+ logger.info(`[PAYSLIP URL] Файл найден для ${tnom} (период: ${period || 'корень'})`);
+ res.json({
+ exists: true,
+ url: fileUrl,
+ tnom: tnom,
+ period: period,
+ fileName: tnom + '.pdf'
+ });
+ } else {
+ logger.warn(`[PAYSLIP URL] Файл не найден для ${tnom} (период: ${period || 'корень'})`);
+ res.json({
+ exists: false,
+ message: 'Расчетный листок не найден',
+ tnom: tnom,
+ period: period
+ });
+ }
+
+ } catch (err) {
+ logger.error('Ошибка получения URL расчетного листка:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// ✅ ОБНОВЛЕННАЯ ОТДАЧА РАСЧЕТНОГО ЛИСТКА (с period)
+app.get('/api/payslip/tnom/:tnom', async function(req, res) {
+ try {
+ var tnom = req.params.tnom;
+ var period = req.query.period || null; // ✅ Берем period из query-параметров
+
+ console.log('==========================================');
+ console.log(`📁 [PAYSLIP] Запрос к /api/payslip/tnom/${tnom}, Период: ${period || 'не указан'}`);
+
+ const filePath = await getPayslipFilePath(tnom, period);
+ console.log(`📁 Полный путь к файлу: ${filePath}`);
+
+ // ✅ ЯВНО проверяем существование файла
+ try {
+ await fsPromises.access(filePath);
+ console.log('✅ Файл найден, начинаем отправку...');
+ } catch (err) {
+ console.log('❌ Файл НЕ найден:', err.message);
+ return res.status(404).json({
+ message: 'Расчетный листок не найден',
+ tnom: tnom,
+ period: period,
+ path: filePath,
+ error: err.message
+ });
+ }
+
+ // ✅ Добавляем правильные заголовки для PDF
+ res.setHeader('Content-Type', 'application/pdf');
+ res.setHeader('Content-Disposition', 'inline; filename="' + tnom + '.pdf"');
+
+ // ✅ Отправляем файл с обработкой ошибок
+ res.sendFile(filePath, function(err) {
+ if (err) {
+ console.error('❌ Ошибка при отправке файла:', err);
+ if (!res.headersSent) {
+ return res.status(500).json({
+ message: 'Ошибка при отправке файла',
+ error: err.message
+ });
+ }
+ } else {
+ console.log('✅ Файл успешно отправлен');
+ }
+ });
+
+ } catch (err) {
+ console.error('❌ Ошибка получения расчетного листка:', err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// ============================================
+// 🗄️ СТАРЫЕ ЭНДПОИНТЫ РАСЧЕТНЫХ ЛИСТКОВ (для обратной совместимости)
+// ============================================
+
+app.get('/api/payslip/:period/:fileName', async function(req, res) {
+ try {
+ var period = req.params.period;
+ var fileName = req.params.fileName;
+ var configPath = path.join(__dirname, '../config.json');
+ var config = await readJson(configPath);
+ var payslipBasePath = config.payslipsPath;
+
+ logger.info('Payslip request - Period: ' + period + ', File: ' + fileName);
+ logger.info('Base path from config: ' + payslipBasePath);
+
+ var networkConnected = await connectToNetworkShare(config);
+ logger.info('Network share connection for payslip: ' + networkConnected);
+
+ var finalPayslipPath;
+ if (path.isAbsolute(payslipBasePath)) {
+ finalPayslipPath = path.join(payslipBasePath, period, fileName);
+ } else {
+ var projectRoot = path.resolve(__dirname, '../../');
+ finalPayslipPath = path.join(projectRoot, payslipBasePath, period, fileName);
+ }
+
+ logger.info('Final payslip path: ' + finalPayslipPath);
+
+ try {
+ var basePathExists = await pathExists(payslipBasePath);
+ logger.info('Base path exists: ' + basePathExists);
+
+ if (basePathExists) {
+ var basePathStats = await fsPromises.stat(payslipBasePath);
+ logger.info('Base path stats: ' + JSON.stringify({
+ isDirectory: basePathStats.isDirectory(),
+ permissions: basePathStats.mode,
+ size: basePathStats.size
+ }));
+
+ var basePathContents = await fsPromises.readdir(payslipBasePath);
+ logger.info('Base path contents: ' + JSON.stringify(basePathContents));
+
+ var periodPath = path.join(payslipBasePath, period);
+ var periodPathExists = await pathExists(periodPath);
+ logger.info('Period path exists: ' + periodPathExists + ' (' + periodPath + ')');
+
+ if (periodPathExists) {
+ var periodPathContents = await fsPromises.readdir(periodPath);
+ logger.info('Period path contents: ' + JSON.stringify(periodPathContents));
+ }
+ }
+ } catch (basePathError) {
+ logger.error('Error checking base path', {
+ error: basePathError.message,
+ stack: basePathError.stack
+ });
+ }
+
+ if (await pathExists(finalPayslipPath)) {
+ logger.info('Payslip found, sending file: ' + finalPayslipPath);
+ res.sendFile(finalPayslipPath);
+ } else {
+ logger.warn('Payslip not found: ' + finalPayslipPath);
+ res.status(404).json({
+ message: 'Расчетный листок не найден',
+ path: finalPayslipPath,
+ period: period,
+ fileName: fileName,
+ basePath: payslipBasePath,
+ networkConnected: networkConnected
+ });
+ }
+ } catch (err) {
+ logger.error('Error getting payslip', { error: err.message, stack: err.stack });
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
+});
+
+app.get('/api/payslip/:fileName', async function(req, res) {
+ try {
+ var fileName = req.params.fileName;
+ var configPath = path.join(__dirname, '../config.json');
+ var config = await readJson(configPath);
+ var payslipBasePath = config.payslipsPath;
+
+ var finalPayslipPath;
+ if (path.isAbsolute(payslipBasePath)) {
+ finalPayslipPath = path.join(payslipBasePath, fileName);
+ } else {
+ var projectRoot = path.resolve(__dirname, '../../');
+ finalPayslipPath = path.join(projectRoot, payslipBasePath, fileName);
+ }
+
+ logger.info('Attempting to find payslip at resolved path: ' + finalPayslipPath);
+
+ if (await pathExists(finalPayslipPath)) {
+ res.sendFile(finalPayslipPath);
+ } else {
+ logger.warn('Payslip not found: ' + finalPayslipPath);
+ res.status(404).json({ message: 'Расчетный листок не найден' });
+ }
+ } catch (err) {
+ logger.error('Error getting payslip', { error: err.message, stack: err.stack });
+ res.status(500).json({ message: 'Ошибка сервера' });
+ }
+});
+
// ============================================
// 🗄️ ДРУГИЕ ЭНДПОИНТЫ
// ============================================
@@ -459,110 +749,6 @@ app.get('/api/test-network', async function(req, res) {
}
});
-app.get('/api/payslip/:period/:fileName', async function(req, res) {
- try {
- var period = req.params.period;
- var fileName = req.params.fileName;
- var configPath = path.join(__dirname, '../config.json');
- var config = await readJson(configPath);
- var payslipBasePath = config.payslipsPath;
-
- logger.info('Payslip request - Period: ' + period + ', File: ' + fileName);
- logger.info('Base path from config: ' + payslipBasePath);
-
- var networkConnected = await connectToNetworkShare(config);
- logger.info('Network share connection for payslip: ' + networkConnected);
-
- var finalPayslipPath;
- if (path.isAbsolute(payslipBasePath)) {
- finalPayslipPath = path.join(payslipBasePath, period, fileName);
- } else {
- var projectRoot = path.resolve(__dirname, '../../');
- finalPayslipPath = path.join(projectRoot, payslipBasePath, period, fileName);
- }
-
- logger.info('Final payslip path: ' + finalPayslipPath);
-
- try {
- var basePathExists = await pathExists(payslipBasePath);
- logger.info('Base path exists: ' + basePathExists);
-
- if (basePathExists) {
- var basePathStats = await fsPromises.stat(payslipBasePath);
- logger.info('Base path stats: ' + JSON.stringify({
- isDirectory: basePathStats.isDirectory(),
- permissions: basePathStats.mode,
- size: basePathStats.size
- }));
-
- var basePathContents = await fsPromises.readdir(payslipBasePath);
- logger.info('Base path contents: ' + JSON.stringify(basePathContents));
-
- var periodPath = path.join(payslipBasePath, period);
- var periodPathExists = await pathExists(periodPath);
- logger.info('Period path exists: ' + periodPathExists + ' (' + periodPath + ')');
-
- if (periodPathExists) {
- var periodPathContents = await fsPromises.readdir(periodPath);
- logger.info('Period path contents: ' + JSON.stringify(periodPathContents));
- }
- }
- } catch (basePathError) {
- logger.error('Error checking base path', {
- error: basePathError.message,
- stack: basePathError.stack
- });
- }
-
- if (await pathExists(finalPayslipPath)) {
- logger.info('Payslip found, sending file: ' + finalPayslipPath);
- res.sendFile(finalPayslipPath);
- } else {
- logger.warn('Payslip not found: ' + finalPayslipPath);
- res.status(404).json({
- message: 'Расчетный листок не найден',
- path: finalPayslipPath,
- period: period,
- fileName: fileName,
- basePath: payslipBasePath,
- networkConnected: networkConnected
- });
- }
- } catch (err) {
- logger.error('Error getting payslip', { error: err.message, stack: err.stack });
- res.status(500).json({ message: 'Ошибка сервера' });
- }
-});
-
-app.get('/api/payslip/:fileName', async function(req, res) {
- try {
- var fileName = req.params.fileName;
- var configPath = path.join(__dirname, '../config.json');
- var config = await readJson(configPath);
- var payslipBasePath = config.payslipsPath;
-
- var finalPayslipPath;
- if (path.isAbsolute(payslipBasePath)) {
- finalPayslipPath = path.join(payslipBasePath, fileName);
- } else {
- var projectRoot = path.resolve(__dirname, '../../');
- finalPayslipPath = path.join(projectRoot, payslipBasePath, fileName);
- }
-
- logger.info('Attempting to find payslip at resolved path: ' + finalPayslipPath);
-
- if (await pathExists(finalPayslipPath)) {
- res.sendFile(finalPayslipPath);
- } else {
- logger.warn('Payslip not found: ' + finalPayslipPath);
- res.status(404).json({ message: 'Расчетный листок не найден' });
- }
- } catch (err) {
- logger.error('Error getting payslip', { error: err.message, stack: err.stack });
- res.status(500).json({ message: 'Ошибка сервера' });
- }
-});
-
app.get('/api/db-status', async function(req, res) {
try {
var db = require('./config/db');
@@ -583,6 +769,13 @@ app.get('/api/time', function(req, res) {
});
});
+// ============================================
+// 📄 РАСЧЕТНЫЙ ЛИСТОК (ФОРМА ДЛЯ ПЕЧАТИ)
+// ============================================
+
+const payslipFormRoutes = require('./routes/payslipForm');
+app.use('/api/payslip', payslipFormRoutes);
+
// ============================================
// 🚀 ЗАПУСК СЕРВЕРА
// ============================================
diff --git a/backend/src/services/excelParser.js b/backend/src/services/excelParser.js
new file mode 100644
index 0000000..4e2b0dd
--- /dev/null
+++ b/backend/src/services/excelParser.js
@@ -0,0 +1,192 @@
+const XLSX = require('xlsx');
+const fs = require('fs');
+const { logger } = require('../config/logger');
+
+/**
+ * Парсит Excel-файл расчетного листка
+ * @param {string} filePath - путь к Excel-файлу
+ * @returns {Object} структурированные данные
+ */
+function parsePayslipExcel(filePath) {
+ try {
+ logger.info('[ExcelParser] Начинаем парсинг:', filePath);
+
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Файл не найден: ${filePath}`);
+ }
+
+ // Читаем Excel
+ const workbook = XLSX.readFile(filePath);
+ const sheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[sheetName];
+ const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+
+ logger.info('[ExcelParser] Загружено строк:', data.length);
+
+ // Инициализируем структуру данных
+ const result = {
+ fullName: '',
+ department: '',
+ position: '',
+ tabNumber: '',
+ daysWorked: '',
+ month: '',
+ year: '',
+ accrued: [],
+ deductions: [],
+ other: [],
+ totalAccrued: 0,
+ totalDeductions: 0,
+ totalPaid: 0,
+ totalOther: 0,
+ bankTransfers: [],
+ advance: '',
+ accountNumber: ''
+ };
+
+ // Парсим построчно
+ data.forEach((row, index) => {
+ const colA = String(row[0] || '').trim();
+ const colB = String(row[1] || '').trim();
+ const colC = String(row[2] || '').trim();
+ const colD = String(row[3] || '').trim();
+ const colE = String(row[4] || '').trim();
+ const colF = String(row[5] || '').trim();
+ const colG = String(row[6] || '').trim();
+
+ // Строка 2: ФИО и Табельный номер (индекс 1)
+ if (index === 1) {
+ if (colB) result.fullName = colB;
+ if (colF) result.tabNumber = colF.replace('Таб.номер', '').trim();
+ }
+
+ // Строка 3: Подразделение
+ if (index === 2 && colB) {
+ result.department = colB;
+ }
+
+ // Строка 4: Должность
+ if (index === 3 && colB) {
+ result.position = colB;
+ }
+
+ // Строка 5: Дни отработано
+ if (index === 4 && colD) {
+ result.daysWorked = colD.replace('Отработано дней:', '').trim();
+ }
+
+ // Строка 0: Месяц и год (заголовок)
+ if (index === 0 && colC) {
+ const monthMatch = colC.match(/за\s+(\w+)\s+(\d{4})/);
+ if (monthMatch) {
+ result.month = monthMatch[1];
+ result.year = monthMatch[2];
+ }
+ }
+
+ // Строка 11: Лицевой счет
+ if (index === 10 && colB && colB.includes('Лиц счет')) {
+ result.accountNumber = colB.replace('Лиц счет =', '').trim();
+ }
+
+ // Парсим начисления (строки 14-17, индекс 13-16)
+ if (index >= 13 && index <= 16 && colB && colG) {
+ const code = colA || '';
+ const description = colB;
+ const period = colC || '';
+ const days = colD || '';
+ const hours = colE || '';
+ const amount = parseFloat(colG.replace(',', '.')) || 0;
+
+ if (description && !description.includes('ВСЕГО')) {
+ result.accrued.push({
+ code,
+ description,
+ period,
+ days,
+ hours,
+ amount
+ });
+ }
+ }
+
+ // Строка 17: ВСЕГО НАЧИСЛЕНО (индекс 17)
+ if (index === 17 && colG) {
+ result.totalAccrued = parseFloat(colG.replace(',', '.')) || 0;
+ }
+
+ // Парсим удержания (строки 19-22, индекс 18-21)
+ if (index >= 18 && index <= 21 && colB && colG) {
+ const code = colA || '';
+ const description = colB;
+ const period = colC || '';
+ const days = colD || '';
+ const hours = colE || '';
+ const amount = parseFloat(colG.replace(',', '.')) || 0;
+
+ if (description && !description.includes('ВСЕГО')) {
+ result.deductions.push({
+ code,
+ description,
+ period,
+ days,
+ hours,
+ amount
+ });
+ }
+ }
+
+ // Строка 22: ВСЕГО УДЕРЖАНО
+ if (index === 22 && colG) {
+ result.totalDeductions = parseFloat(colG.replace(',', '.')) || 0;
+ }
+
+ // Строка 23: Аванс
+ if (index === 23 && colB && colB.includes('Аванс')) {
+ result.advance = colF || '';
+ }
+
+ // Строки 24-25: Банковские переводы
+ if (index >= 24 && index <= 25 && colB && colC) {
+ const bankName = colB.replace('Отчислено в банк', '').trim();
+ const amount = parseFloat(colC.replace(',', '.')) || 0;
+ if (bankName && amount > 0) {
+ result.bankTransfers.push({ bank: bankName, amount });
+ }
+ }
+
+ // Строка 26: ВСЕГО ВЫПЛАЧЕНО
+ if (index === 26 && colG) {
+ result.totalPaid = parseFloat(colG.replace(',', '.')) || 0;
+ }
+
+ // Строка 28: Прочее
+ if (index >= 28 && index <= 29 && colB && colG) {
+ const code = colA || '';
+ const description = colB;
+ const amount = parseFloat(colG.replace(',', '.')) || 0;
+ if (description && !description.includes('ВСЕГО')) {
+ result.other.push({
+ code,
+ description,
+ amount
+ });
+ }
+ }
+
+ // Строка 30: ВСЕГО ПРОЧЕЕ
+ if (index === 30 && colG) {
+ result.totalOther = parseFloat(colG.replace(',', '.')) || 0;
+ }
+ });
+
+ logger.info('[ExcelParser] Парсинг завершен успешно');
+ return result;
+
+ } catch (error) {
+ logger.error('[ExcelParser] Ошибка парсинга:', error.message);
+ throw error;
+ }
+}
+
+module.exports = { parsePayslipExcel };
\ No newline at end of file
diff --git a/backend/src/services/formGenerator.js b/backend/src/services/formGenerator.js
new file mode 100644
index 0000000..14274be
--- /dev/null
+++ b/backend/src/services/formGenerator.js
@@ -0,0 +1,258 @@
+const { logger } = require('../config/logger');
+
+/**
+ * Генерирует HTML-форму расчетного листка для термопринтера
+ * @param {Object} data - данные из Excel
+ * @returns {string} HTML-код формы
+ */
+function generatePayslipForm(data) {
+ try {
+ logger.info('[FormGenerator] Генерация HTML-формы');
+
+ const monthNames = {
+ 'Январь': '01', 'Февраль': '02', 'Март': '03', 'Апрель': '04',
+ 'Май': '05', 'Июнь': '06', 'Июль': '07', 'Август': '08',
+ 'Сентябрь': '09', 'Октябрь': '10', 'Ноябрь': '11', 'Декабрь': '12'
+ };
+
+ const monthNum = monthNames[data.month] || '00';
+ const period = `${monthNum}${data.year || '2026'}`;
+
+ // Формируем HTML для термопринтера (80мм ширина)
+ let html = `
+
+
+
+
+ Расчетный листок
+
+
+
+
+
+
+
+
+ ФИО:
+ ${data.fullName || ''}
+
+
+ Таб.№:
+ ${data.tabNumber || ''}
+
+
+ Подразделение:
+ ${data.department || ''}
+
+
+ Должность:
+ ${data.position || ''}
+
+
+ Отработано:
+ ${data.daysWorked || ''}
+
+
+
+
+
+ 📈 НАЧИСЛЕНО
+ ${data.accrued.map(item => `
+
+ ${item.code || ''} ${item.description || ''}
+ ${item.amount.toFixed(2)}
+
+ `).join('')}
+
+ ИТОГО НАЧИСЛЕНО:
+ ${data.totalAccrued.toFixed(2)}
+
+
+
+
+
+ 📉 УДЕРЖАНО
+ ${data.deductions.map(item => `
+
+ ${item.code || ''} ${item.description || ''}
+ ${item.amount.toFixed(2)}
+
+ `).join('')}
+
+ ИТОГО УДЕРЖАНО:
+ ${data.totalDeductions.toFixed(2)}
+
+
+
+
+
+
+ 💰 К ВЫПЛАТЕ:
+ ${data.totalPaid.toFixed(2)}
+
+
+ ${data.advance ? `
+
+ Аванс:
+ ${data.advance}
+
+ ` : ''}
+
+
+ ${data.bankTransfers.length > 0 ? `
+
+ 🏦 БАНКОВСКИЕ ПЕРЕВОДЫ
+ ${data.bankTransfers.map(bank => `
+
+ ${bank.bank || ''}
+ ${bank.amount.toFixed(2)}
+
+ `).join('')}
+ ` : ''}
+
+
+ ${data.accountNumber ? `
+
+
+ Лицевой счет:
+ ${data.accountNumber}
+
+ ` : ''}
+
+
+ ${data.other.length > 0 ? `
+
+ 📌 ПРОЧЕЕ
+ ${data.other.map(item => `
+
+ ${item.code || ''} ${item.description || ''}
+ ${item.amount.toFixed(2)}
+
+ `).join('')}
+
+ ИТОГО ПРОЧЕЕ:
+ ${data.totalOther.toFixed(2)}
+
+ ` : ''}
+
+
+
+`;
+
+ logger.info('[FormGenerator] HTML-форма сгенерирована');
+ return html;
+
+ } catch (error) {
+ logger.error('[FormGenerator] Ошибка генерации:', error.message);
+ throw error;
+ }
+}
+
+module.exports = { generatePayslipForm };
\ No newline at end of file
diff --git a/frontend/GP-80250 Series b/frontend/GP-80250 Series
new file mode 100644
index 0000000..16925cd
Binary files /dev/null and b/frontend/GP-80250 Series differ
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ce32cde..b84a8d0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,20 +13,25 @@
"@testing-library/user-event": "^13.5.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
- "axios": "^1.19.0",
+ "axios": "^1.20.0",
"date-fns": "^2.30.0",
"dompurify": "^3.4.13",
"dotenv": "^17.4.2",
"electron-is-dev": "^2.0.0",
"framer-motion": "^13.1.0",
+ "pdf-lib": "^1.17.1",
+ "pdf-to-printer": "^5.8.1",
"pdfjs-dist": "^3.11.174",
+ "pdfkit": "^0.20.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^4.11.0",
"react-pdf": "^8.0.0",
"react-zoom-pan-pinch": "^3.3.0",
+ "sharp": "^0.35.4",
"typescript": "^5.8.0",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "xlsx": "^0.18.5"
},
"devDependencies": {
"@types/jest": "^29.5.14",
@@ -508,6 +513,16 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -532,6 +547,554 @@
"@hapi/hoek": "^9.0.0"
}
},
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
+ "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
+ "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
+ "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
+ "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
+ "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
+ "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
+ "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
+ "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
+ "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
+ "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
+ "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
+ "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
+ "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
+ "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
+ "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
+ "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
+ "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
+ "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
+ "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
+ "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
+ "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
+ "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
+ "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
+ "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
+ "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
+ "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
@@ -895,6 +1458,18 @@
"node": "^22.20 || ^24.12 || >=25"
}
},
+ "node_modules/@noble/ciphers": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@noble/hashes": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz",
@@ -990,6 +1565,24 @@
"url": "https://github.com/sponsors/Boshen"
}
},
+ "node_modules/@pdf-lib/standard-fonts": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
+ "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.6"
+ }
+ },
+ "node_modules/@pdf-lib/upng": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
+ "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.10"
+ }
+ },
"node_modules/@peculiar/asn1-schema": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz",
@@ -1681,6 +2274,15 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@@ -2148,6 +2750,15 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -2515,9 +3126,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.19.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
- "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@@ -2540,7 +3151,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2611,6 +3221,15 @@
"node": ">=8"
}
},
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -3056,6 +3675,19 @@
"node": ">=6"
}
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -3229,6 +3861,15 @@
"node": ">=6"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3340,6 +3981,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-dirname": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
@@ -3561,7 +4214,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -3575,6 +4227,12 @@
"license": "MIT",
"optional": true
},
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
+ "license": "MIT"
+ },
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -4493,7 +5151,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
@@ -4523,6 +5180,12 @@
"pend": "~1.2.0"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT"
+ },
"node_modules/filelist": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
@@ -4596,6 +5259,32 @@
}
}
},
+ "node_modules/fontkit": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
+ "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.12",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "dfa": "^1.2.0",
+ "fast-deep-equal": "^3.1.3",
+ "restructure": "^3.0.0",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.4.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/fontkit/node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
@@ -4612,6 +5301,15 @@
"node": ">= 6"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/framer-motion": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.0.tgz",
@@ -5858,6 +6556,25 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
@@ -6781,6 +7498,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -6828,6 +7551,30 @@
"node": ">= 14.16"
}
},
+ "node_modules/pdf-lib": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
+ "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pdf-lib/standard-fonts": "^1.0.0",
+ "@pdf-lib/upng": "^1.0.1",
+ "pako": "^1.0.11",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/pdf-lib/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/pdf-to-printer": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/pdf-to-printer/-/pdf-to-printer-5.8.1.tgz",
+ "integrity": "sha512-bVMrGbBiVw8G/z0b28DhMguoJa8rqbVcbfLtpQzGUJ7eaq188JA2AxsFeuFQwWxAvMCa1Hyh8vXc2QybQVrPrw==",
+ "license": "MIT"
+ },
"node_modules/pdfjs-dist": {
"version": "3.11.174",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz",
@@ -6841,6 +7588,32 @@
"path2d-polyfill": "^2.0.1"
}
},
+ "node_modules/pdfkit": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.20.2.tgz",
+ "integrity": "sha512-Q/w03ICAQyXfHNfTsg1udp0ADerdBN0s7a6XSPL7J7Ro6ABnafoBOCDBstIO9U02mvzHEV8HrvFIw5iV5ejIRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/ciphers": "^1.3.0",
+ "@noble/hashes": "^1.8.0",
+ "fflate": "^0.8.3",
+ "fontkit": "^2.0.4",
+ "linebreak": "^1.1.0",
+ "png-js": "^2.0.0"
+ }
+ },
+ "node_modules/pdfkit/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/pe-library": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
@@ -6928,6 +7701,14 @@
"node": ">=10.4.0"
}
},
+ "node_modules/png-js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/png-js/-/png-js-2.0.0.tgz",
+ "integrity": "sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==",
+ "dependencies": {
+ "fflate": "^0.8.2"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
@@ -7321,6 +8102,12 @@
"node": ">=8"
}
},
+ "node_modules/restructure": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
+ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
+ "license": "MIT"
+ },
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -7550,6 +8337,67 @@
"devOptional": true,
"license": "ISC"
},
+ "node_modules/sharp": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
+ "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.4",
+ "@img/sharp-darwin-x64": "0.35.4",
+ "@img/sharp-freebsd-wasm32": "0.35.4",
+ "@img/sharp-libvips-darwin-arm64": "1.3.3",
+ "@img/sharp-libvips-darwin-x64": "1.3.3",
+ "@img/sharp-libvips-linux-arm": "1.3.3",
+ "@img/sharp-libvips-linux-arm64": "1.3.3",
+ "@img/sharp-libvips-linux-ppc64": "1.3.3",
+ "@img/sharp-libvips-linux-riscv64": "1.3.3",
+ "@img/sharp-libvips-linux-s390x": "1.3.3",
+ "@img/sharp-libvips-linux-x64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3",
+ "@img/sharp-linux-arm": "0.35.4",
+ "@img/sharp-linux-arm64": "0.35.4",
+ "@img/sharp-linux-ppc64": "0.35.4",
+ "@img/sharp-linux-riscv64": "0.35.4",
+ "@img/sharp-linux-s390x": "0.35.4",
+ "@img/sharp-linux-x64": "0.35.4",
+ "@img/sharp-linuxmusl-arm64": "0.35.4",
+ "@img/sharp-linuxmusl-x64": "0.35.4",
+ "@img/sharp-webcontainers-wasm32": "0.35.4",
+ "@img/sharp-win32-arm64": "0.35.4",
+ "@img/sharp-win32-ia32": "0.35.4",
+ "@img/sharp-win32-x64": "0.35.4"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7794,6 +8642,18 @@
"license": "BSD-3-Clause",
"optional": true
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/ssri": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
@@ -8057,6 +8917,12 @@
"semver": "bin/semver"
}
},
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -8265,6 +9131,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "license": "MIT"
+ },
"node_modules/unique-filename": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
@@ -9684,6 +10576,24 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -9709,6 +10619,27 @@
"devOptional": true,
"license": "ISC"
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 797b067..e5e961e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,20 +9,25 @@
"@testing-library/user-event": "^13.5.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
- "axios": "^1.19.0",
+ "axios": "^1.20.0",
"date-fns": "^2.30.0",
"dompurify": "^3.4.13",
"dotenv": "^17.4.2",
"electron-is-dev": "^2.0.0",
"framer-motion": "^13.1.0",
+ "pdf-lib": "^1.17.1",
+ "pdf-to-printer": "^5.8.1",
"pdfjs-dist": "^3.11.174",
+ "pdfkit": "^0.20.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^4.11.0",
"react-pdf": "^8.0.0",
"react-zoom-pan-pinch": "^3.3.0",
+ "sharp": "^0.35.4",
"typescript": "^5.8.0",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "xlsx": "^0.18.5"
},
"scripts": {
"start": "vite",
diff --git a/frontend/public/assets/images/background.jpg b/frontend/public/assets/images/background.jpg
new file mode 100644
index 0000000..068a916
Binary files /dev/null and b/frontend/public/assets/images/background.jpg differ
diff --git a/frontend/public/electron.js b/frontend/public/electron.js
index abb12bc..d55deee 100644
--- a/frontend/public/electron.js
+++ b/frontend/public/electron.js
@@ -3,11 +3,11 @@ const path = require('path');
require('dotenv').config({path: path.join(__dirname, 'config.env')})
const isDev = require('electron-is-dev');
const fs = require('fs');
-const COM_PORT = process.env.COM_PORT;
-const COM_SPEED = process.env.COM_SPEED;
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 { print } = require('pdf-to-printer');
+
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
@@ -64,13 +64,12 @@ function createWindow() {
mainWindow = new BrowserWindow({
kiosk: true,
- titleBarStyle:'hidden',
+ titleBarStyle: 'hidden',
alwaysOnTop: false,
webPreferences: {
preload: preloadPath,
contextIsolation: true,
nodeIntegration: false
-
},
});
@@ -91,6 +90,98 @@ function createWindow() {
});
}
+// ============================================
+// 📄 ПЕЧАТЬ PDF ЧЕРЕЗ pdf-to-printer
+// ============================================
+
+ipcMain.handle('print-pdf', async (event, data = {}) => {
+ let tempFile = null;
+
+ try {
+ if (!data.pdfUrl) {
+ throw new Error('URL файла PDF не передан');
+ }
+
+ const cleanUrl = data.pdfUrl.split('#')[0];
+ logger.info('[PRINT] Загрузка PDF:', cleanUrl);
+
+ // ✅ Скачиваем PDF
+ const response = await axios.get(cleanUrl, {
+ responseType: 'arraybuffer',
+ timeout: 30000
+ });
+
+ const pdfBuffer = Buffer.from(response.data);
+ logger.info('[PRINT] Размер PDF:', pdfBuffer.length, 'байт');
+
+ if (pdfBuffer.length < 100) {
+ throw new Error('PDF слишком маленький или пустой');
+ }
+
+ // ✅ Сохраняем во временный файл
+ const tempDir = app.getPath('temp');
+ tempFile = path.join(tempDir, `print_${Date.now()}.pdf`);
+ fs.writeFileSync(tempFile, pdfBuffer);
+ logger.info('[PRINT] Временный файл создан:', tempFile);
+
+ // ✅ ПЕЧАТАЕМ ЧЕРЕЗ pdf-to-printer
+ const printerName = 'GP-80250 Series';
+ logger.info('[PRINT] Отправляем на печать через pdf-to-printer:', printerName);
+
+ // ✅ Опции печати
+ const options = {
+ printer: printerName,
+ // scale: 'fit', // Масштабирование
+ // paperSize: 'A4',
+ // landscape: false,
+ // copies: 1,
+ // silent: true // Тихая печать
+ };
+
+ const result = await print(tempFile, options);
+ logger.info('[PRINT] Результат печати:', result);
+
+ // ✅ Удаляем временный файл
+ try {
+ if (tempFile && fs.existsSync(tempFile)) {
+ fs.unlinkSync(tempFile);
+ logger.info('[PRINT] Временный файл удален');
+ }
+ } catch (e) {}
+
+ logger.info('[PRINT] ✅ Документ отправлен на печать');
+ return true;
+
+ } catch (error) {
+ logger.error('[PRINT] ❌ Ошибка:', error.message);
+ try {
+ if (tempFile && fs.existsSync(tempFile)) {
+ fs.unlinkSync(tempFile);
+ }
+ } catch (e) {}
+ throw error;
+ }
+});
+
+// ============================================
+// 📄 ПОЛУЧЕНИЕ СПИСКА ПРИНТЕРОВ
+// ============================================
+
+ipcMain.handle('get-printers', async () => {
+ return [
+ {
+ name: 'GP-80250 Series',
+ displayName: 'GP-80250 Series',
+ isDefault: true,
+ status: 'ready'
+ }
+ ];
+});
+
+// ============================================
+// 🚀 ЗАПУСК
+// ============================================
+
app.whenReady().then(() => {
createWindow();
global.allowMinimize = false;
@@ -112,14 +203,13 @@ app.whenReady().then(() => {
}
}, 10);
});
+
globalShortcut.register('Control+Alt+Enter', () => {
global.allowClose = true;
app.quit();
});
- globalShortcut.register('F11', () => {
- });
-
+ globalShortcut.register('F11', () => {});
});
app.on('window-all-closed', () => {
@@ -138,12 +228,6 @@ app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
-//считыватель
-
ipcMain.handle('get-app-config', async (event) => {
- console.log('[IPC] Renderer requested app config. Sending:', appConfig);
- return appConfig;
-});
-
-app.whenReady().then(() => {
+ return appConfig;
});
\ No newline at end of file
diff --git a/frontend/public/payslips/012025/23959.pdf b/frontend/public/payslips/012025/23959.pdf
deleted file mode 100644
index a4ad00d..0000000
Binary files a/frontend/public/payslips/012025/23959.pdf and /dev/null differ
diff --git a/frontend/public/payslips/test.pdf b/frontend/public/payslips/test.pdf
deleted file mode 100644
index a4ad00d..0000000
Binary files a/frontend/public/payslips/test.pdf and /dev/null differ
diff --git a/frontend/public/preload.js b/frontend/public/preload.js
index 9f69592..223cfbb 100644
--- a/frontend/public/preload.js
+++ b/frontend/public/preload.js
@@ -1,6 +1,12 @@
-const { logger } = require('../../backend/src/config/logger');
-logger.info('[Preload] Script started.');
const { contextBridge, ipcRenderer } = require('electron');
+
+try {
+ const { logger } = require('../../backend/src/config/logger');
+ logger.info('[Preload] Script started.');
+} catch (e) {
+ console.error('[Preload] Логгер не загрузился, но едем дальше.', e);
+}
+
contextBridge.exposeInMainWorld(
'electronAPI',
{
@@ -8,12 +14,13 @@ contextBridge.exposeInMainWorld(
onCardDetected: (callback) => {
ipcRenderer.on('card-detected', (event, uid) => callback(uid));
},
-
removeCardDetectedListeners: () => {
ipcRenderer.removeAllListeners('card-detected');
- }
+ },
+ printPdf: (options) => ipcRenderer.invoke('print-pdf', options),
+ getPrinters: () => ipcRenderer.invoke('get-printers'),
}
);
-logger.log('[Preload] Script finished. electronAPI exposed.');
+console.log('[Preload] Script finished. electronAPI exposed.');
window.dispatchEvent(new Event('electronApiReady'));
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b5e37d0..2815b8c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -45,6 +45,7 @@ function App() {
const [currentSection, setCurrentSection] = useState(null);
const [showPayslip, setShowPayslip] = useState(false);
const [selectedPeriod, setSelectedPeriod] = useState('');
+ const [payslipFileUrl, setPayslipFileUrl] = useState('');
// 📊 Состояния для таймера
const [showInactiveWarning, setShowInactiveWarning] = useState(false);
@@ -64,6 +65,7 @@ function App() {
setCurrentSection(null);
setShowPayslip(false);
setSelectedPeriod('');
+ setPayslipFileUrl('');
setShowInactiveWarning(false);
setLoginError('');
@@ -138,6 +140,7 @@ function App() {
setCurrentSection(null);
setShowPayslip(false);
setSelectedPeriod('');
+ setPayslipFileUrl('');
handleUserActivity();
};
@@ -146,8 +149,8 @@ function App() {
handleUserActivity();
};
- const handleViewPayslip = (period: string) => {
- setSelectedPeriod(period);
+ const handleViewPayslip = (fileUrl: string) => {
+ setPayslipFileUrl(fileUrl);
setShowPayslip(true);
handleUserActivity();
};
@@ -155,14 +158,10 @@ function App() {
const handlePayslipBack = () => {
setShowPayslip(false);
setSelectedPeriod('');
+ setPayslipFileUrl('');
handleUserActivity();
};
- const getPayslipUrl = (employee: Employee, period: string) => {
- const fileName = `${employee.tnom}.pdf`;
- return `${config.backendUrl}/api/payslip/${period}/${fileName}`;
- };
-
// ============================================
// ⚠️ Предупреждение о неактивности
// ============================================
@@ -279,7 +278,7 @@ function App() {
if (showPayslip) {
return (
);
@@ -316,7 +315,7 @@ function App() {
// 🚀 Рендер приложения
// ============================================
const showTopBar = !showVideo && !showPayslip;
- const topBarHeight = 70; // высота виджета
+ const topBarHeight = 70;
return (
+
)}
{/* 📄 Основной контент с отступом сверху */}
@@ -348,7 +347,7 @@ function App() {
{renderContent()}
- {/* 📊 НИЖНЯЯ ПАНЕЛЬ: КУРСЫ ВАЛЮТ */}
+ {/* 📊 НИЖНЯЯ ПАНЕЛЬ: ТОЛЬКО КУРСЫ ВАЛЮТ (БЕЗ ПРИВЕТСТВИЯ) */}
{showTopBar && (