From 70a20665ae29d8eb8ee9c4b30703d6bc14331e81 Mon Sep 17 00:00:00 2001 From: tolya Date: Tue, 1 Sep 2026 11:47:15 +0300 Subject: [PATCH] print_add --- admin-frontend/src/components/Editor.jsx | 164 +-- .../src/components/SlidesManager.jsx | 395 ++++++++ admin-server1/logs/admin.log | 319 ++++++ admin-server1/moders/routes/content.js | 244 +++-- .../uploads/1787925962367-682893.png | Bin 0 -> 8837 bytes .../uploads/1787926013867-685930.png | Bin 0 -> 8837 bytes .../uploads/1788176541470-547073.png | Bin 0 -> 18172 bytes .../uploads/1788183146109-428535.png | Bin 0 -> 58965 bytes .../uploads/1788184329751-840142.png | Bin 0 -> 822754 bytes .../uploads/1788184340940-777129.png | Bin 0 -> 822754 bytes .../uploads/1788184553043-530019.png | Bin 0 -> 18172 bytes backend/config.json | 13 +- backend/payslips/022026/002.pdf | Bin 0 -> 70488 bytes backend/payslips/042026/003.pdf | Bin 0 -> 70964 bytes backend/payslips/112025/001.pdf | Bin 0 -> 155376 bytes backend/payslips/112025/001.xlsx | Bin 0 -> 11622 bytes backend/src/routes/payslipForm.js | 117 +++ backend/src/server.js | 405 ++++++-- backend/src/services/excelParser.js | 192 ++++ backend/src/services/formGenerator.js | 258 +++++ frontend/GP-80250 Series | Bin 0 -> 70092 bytes frontend/package-lock.json | 947 +++++++++++++++++- frontend/package.json | 9 +- frontend/public/assets/images/background.jpg | Bin 0 -> 191243 bytes frontend/public/electron.js | 114 ++- frontend/public/payslips/012025/23959.pdf | Bin 672232 -> 0 bytes frontend/public/payslips/test.pdf | Bin 672232 -> 0 bytes frontend/public/preload.js | 17 +- frontend/src/App.tsx | 23 +- frontend/src/components/Contacts.tsx | 317 +++--- frontend/src/components/EmployeeInfo.tsx | 733 +++++++++++++- frontend/src/components/Finance.tsx | 786 ++++++++++++--- frontend/src/components/MenuComponent.tsx | 290 ++++-- frontend/src/components/PayslipViewer.tsx | 249 ++++- frontend/src/components/VideoBackground.tsx | 420 +++++++- frontend/src/components/WeatherWidget.tsx | 355 ++++--- frontend/src/react-app-env.d.ts | 14 + frontend/tsconfig.tsbuildinfo | 2 +- 38 files changed, 5494 insertions(+), 889 deletions(-) create mode 100644 admin-frontend/src/components/SlidesManager.jsx create mode 100644 admin-server1/uploads/1787925962367-682893.png create mode 100644 admin-server1/uploads/1787926013867-685930.png create mode 100644 admin-server1/uploads/1788176541470-547073.png create mode 100644 admin-server1/uploads/1788183146109-428535.png create mode 100644 admin-server1/uploads/1788184329751-840142.png create mode 100644 admin-server1/uploads/1788184340940-777129.png create mode 100644 admin-server1/uploads/1788184553043-530019.png create mode 100644 backend/payslips/022026/002.pdf create mode 100644 backend/payslips/042026/003.pdf create mode 100644 backend/payslips/112025/001.pdf create mode 100644 backend/payslips/112025/001.xlsx create mode 100644 backend/src/routes/payslipForm.js create mode 100644 backend/src/services/excelParser.js create mode 100644 backend/src/services/formGenerator.js create mode 100644 frontend/GP-80250 Series create mode 100644 frontend/public/assets/images/background.jpg delete mode 100644 frontend/public/payslips/012025/23959.pdf delete mode 100644 frontend/public/payslips/test.pdf 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 === '


') return; + e.preventDefault(); + const content = DOMPurify.sanitize(editContent.trim()); + if (!content || content === '


') return; - const url = editingBlockId - ? `/moders/content/${activeTab}/${editingBlockId}` - : `/moders/content/${activeTab}`; + const url = editingBlockId + ? `/moders/content/${activeTab}/${editingBlockId}` + : `/moders/content/${activeTab}`; - const method = editingBlockId ? 'PUT' : 'POST'; + const method = editingBlockId ? 'PUT' : 'POST'; - console.log('➡️ Отправка:', method, url, { type: 'text', content }); + try { + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type: 'text', content }) + }); - try { - const response = await fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ type: 'text', content }) - }); - - const data = await response.json(); - - console.log('⬅️ Ответ сервера:', response.status, data); - - if (response.ok) { - showNotify(editingBlockId ? 'Блок обновлён' : 'Блок добавлен', 'success'); - setShowEditModal(false); - loadBlocks(activeTab); - } else { - showNotify(`Ошибка: ${data.message || response.status}`, 'error'); + if (response.ok) { + showNotify(editingBlockId ? 'Блок обновлён' : 'Блок добавлен', 'success'); + setShowEditModal(false); + loadBlocks(activeTab); + } else { + const data = await response.json(); + showNotify(`Ошибка: ${data.message || response.status}`, 'error'); + } + } catch (e) { + showNotify('Ошибка сохранения', 'error'); } - } catch (e) { - console.error('❌ Ошибка при сохранении:', e); - showNotify('Ошибка сохранения', 'error'); - } -}; + }; const handleTryCloseEditModal = () => { if (editContent !== originalContentRef.current) { @@ -194,7 +177,6 @@ const authenticatedFetch = useCallback((url, options = {}) => { // Initialize Quill when modal opens useEffect(() => { if (showEditModal && editorRef.current && !quillRef.current) { - // Dynamically import Quill import('quill').then((QuillModule) => { const Quill = QuillModule.default || QuillModule; if (editorRef.current) { @@ -225,51 +207,68 @@ const authenticatedFetch = useCallback((url, options = {}) => { }); } - // Cleanup Quill when modal closes if (!showEditModal && quillRef.current) { quillRef.current = null; } }, [showEditModal, editContent]); + // Если не авторизован — показываем форму входа + if (!isLoggedIn) { + return ( +
+

Панель модератора

+
+

Вход для модератора

+
+
+ + setUsername(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+ + {loginError &&

{loginError}

} +
+
+
+ ); + } + + // Авторизован — показываем редактор return ( <>
{notify.message}

Панель модератора

- {!isLoggedIn ? ( -
-

Вход для модератора

-
-
- - setUsername(e.target.value)} /> -
-
- - setPassword(e.target.value)} /> -
- - {loginError &&

{loginError}

} -
-
+ +

Редактирование вкладок

+ +
+ + + + + {/* ✅ НОВАЯ ВКЛАДКА СЛАЙДЫ */} + +
+ + {/* Рендеринг контента вкладок */} + {activeTab === 'slides' ? ( + ) : ( -
- -

Редактирование вкладок

-
- - - - -
+ <>
@@ -283,9 +282,10 @@ const authenticatedFetch = useCallback((url, options = {}) => {
))}
- + )} + {/* Модалка редактирования блоков */} {showEditModal && (
{ 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 => ( +
+ {slide.title { + 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 ? 'Редактировать слайд' : 'Добавить слайд'} +

+ +
+
+ +
+ setFormData(prev => ({ ...prev, image_url: e.target.value }))} + placeholder="https://example.com/image.jpg" + style={{ flex: 1, padding: '8px 12px', borderRadius: '8px', border: '1px solid #ddd' }} + /> + +
+
+ +
+ + setFormData(prev => ({ ...prev, title: e.target.value }))} + placeholder="Заголовок слайда" + style={{ width: '100%', padding: '8px 12px', borderRadius: '8px', border: '1px solid #ddd' }} + /> +
+ +
+ +