import React, { useState, useEffect, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { Icon } from "@iconify/react"; import { useAppSelector, useAppDispatch } from "../store/hooks"; import { userApi } from "../api/userApi"; import { authApi } from "../api/authApi"; import { notesApi, logsApi, Log } from "../api/notesApi"; import { Note } from "../types/note"; import { setUser, setAiSettings } from "../store/slices/profileSlice"; import { setAccentColor as setAccentColorAction } from "../store/slices/uiSlice"; import { clearAuth } from "../store/slices/authSlice"; import { setAccentColor } from "../utils/colorUtils"; import { useNotification } from "../hooks/useNotification"; import { Modal } from "../components/common/Modal"; import { ThemeToggle } from "../components/common/ThemeToggle"; import { formatDateFromTimestamp } from "../utils/dateFormat"; import { parseMarkdown } from "../utils/markdown"; type SettingsTab = "appearance" | "ai" | "archive" | "logs"; const SettingsPage: React.FC = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { showNotification } = useNotification(); const user = useAppSelector((state) => state.profile.user); const accentColor = useAppSelector((state) => state.ui.accentColor); const [activeTab, setActiveTab] = useState("appearance"); // Appearance settings const [selectedAccentColor, setSelectedAccentColor] = useState("#007bff"); // AI settings const [apiKey, setApiKey] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [model, setModel] = useState(""); const [aiEnabled, setAiEnabled] = useState(false); // Archive const [archivedNotes, setArchivedNotes] = useState([]); const [isLoadingArchived, setIsLoadingArchived] = useState(false); // Logs const [logs, setLogs] = useState([]); const [logsOffset, setLogsOffset] = useState(0); const [hasMoreLogs, setHasMoreLogs] = useState(true); const [logTypeFilter, setLogTypeFilter] = useState(""); const [isLoadingLogs, setIsLoadingLogs] = useState(false); // Delete all archived modal const [isDeleteAllModalOpen, setIsDeleteAllModalOpen] = useState(false); const [deleteAllPassword, setDeleteAllPassword] = useState(""); const [isDeletingAll, setIsDeletingAll] = useState(false); const logsLimit = 50; const colorOptions = [ { color: "#007bff", title: "Синий" }, { color: "#28a745", title: "Зеленый" }, { color: "#dc3545", title: "Красный" }, { color: "#fd7e14", title: "Оранжевый" }, { color: "#6f42c1", title: "Фиолетовый" }, { color: "#e83e8c", title: "Розовый" }, ]; useEffect(() => { loadUserInfo(); }, []); useEffect(() => { if (activeTab === "archive") { loadArchivedNotes(); } else if (activeTab === "logs") { loadLogs(true); } else if (activeTab === "ai") { loadAiSettings(); } }, [activeTab]); const loadUserInfo = async () => { try { const userData = await userApi.getProfile(); dispatch(setUser(userData)); const accent = userData.accent_color || "#007bff"; setSelectedAccentColor(accent); dispatch(setAccentColorAction(accent)); setAccentColor(accent); // Загружаем AI настройки try { const aiSettings = await userApi.getAiSettings(); dispatch(setAiSettings(aiSettings)); } catch (aiError) { console.error("Ошибка загрузки AI настроек:", aiError); } } catch (error) { console.error("Ошибка загрузки информации о пользователе:", error); } }; const loadAiSettings = async () => { try { const settings = await userApi.getAiSettings(); setApiKey(settings.openai_api_key || ""); setBaseUrl(settings.openai_base_url || ""); setModel(settings.openai_model || ""); setAiEnabled(settings.ai_enabled === 1); localStorage.setItem("ai_enabled", settings.ai_enabled ? "1" : "0"); } catch (error) { console.error("Ошибка загрузки AI настроек:", error); } }; const handleUpdateAppearance = async () => { try { await userApi.updateProfile({ accent_color: selectedAccentColor, }); dispatch(setAccentColorAction(selectedAccentColor)); setAccentColor(selectedAccentColor); await loadUserInfo(); showNotification("Цветовой акцент успешно обновлен", "success"); } catch (error: any) { console.error("Ошибка обновления цветового акцента:", error); showNotification( error.response?.data?.error || "Ошибка обновления", "error" ); } }; const handleUpdateAiSettings = async () => { if (!apiKey.trim()) { showNotification("API ключ обязателен", "error"); return; } if (!baseUrl.trim()) { showNotification("Base URL обязателен", "error"); return; } if (!model.trim()) { showNotification("Название модели обязательно", "error"); return; } try { await userApi.updateAiSettings({ openai_api_key: apiKey, openai_base_url: baseUrl, openai_model: model, }); showNotification("AI настройки успешно сохранены", "success"); updateAiToggleState(); } catch (error: any) { console.error("Ошибка сохранения AI настроек:", error); showNotification( error.response?.data?.error || "Ошибка сохранения", "error" ); } }; const handleAiToggleChange = async (checked: boolean) => { if (checked && !checkAiSettingsFilled()) { showNotification("Сначала заполните все AI настройки", "warning"); return; } try { await userApi.updateAiSettings({ ai_enabled: checked ? 1 : 0, }); setAiEnabled(checked); localStorage.setItem("ai_enabled", checked ? "1" : "0"); showNotification( checked ? "Помощь ИИ включена" : "Помощь ИИ отключена", "success" ); } catch (error: any) { console.error("Ошибка сохранения настройки AI:", error); showNotification( error.response?.data?.error || "Ошибка сохранения", "error" ); setAiEnabled(!checked); } }; const checkAiSettingsFilled = () => { return apiKey.trim() && baseUrl.trim() && model.trim(); }; const updateAiToggleState = () => { const isFilled = checkAiSettingsFilled(); if (!isFilled) { setAiEnabled(false); } }; const loadArchivedNotes = async () => { setIsLoadingArchived(true); try { const notes = await notesApi.getArchived(); setArchivedNotes(notes); } catch (error) { console.error("Ошибка загрузки архивных заметок:", error); showNotification("Ошибка загрузки архивных заметок", "error"); } finally { setIsLoadingArchived(false); } }; const handleRestoreNote = async (id: number) => { try { await notesApi.unarchive(id); await loadArchivedNotes(); showNotification("Заметка восстановлена!", "success"); } catch (error: any) { console.error("Ошибка восстановления заметки:", error); showNotification( error.response?.data?.error || "Ошибка восстановления", "error" ); } }; const handleDeletePermanent = async (id: number) => { try { await notesApi.deleteArchived(id); await loadArchivedNotes(); showNotification("Заметка удалена окончательно", "success"); } catch (error: any) { console.error("Ошибка удаления заметки:", error); showNotification( error.response?.data?.error || "Ошибка удаления", "error" ); } }; const handleDeleteAllArchived = async () => { if (!deleteAllPassword.trim()) { showNotification("Введите пароль", "warning"); return; } setIsDeletingAll(true); try { await notesApi.deleteAllArchived(deleteAllPassword); showNotification("Все архивные заметки удалены", "success"); setIsDeleteAllModalOpen(false); setDeleteAllPassword(""); await loadArchivedNotes(); } catch (error: any) { console.error("Ошибка:", error); showNotification( error.response?.data?.error || "Ошибка удаления", "error" ); } finally { setIsDeletingAll(false); } }; const loadLogs = useCallback( async (reset = false) => { setIsLoadingLogs(true); try { const offset = reset ? 0 : logsOffset; const newLogs = await logsApi.getLogs({ action_type: logTypeFilter || undefined, limit: logsLimit, offset: offset, }); if (reset) { setLogs(newLogs); setLogsOffset(newLogs.length); } else { setLogs((prevLogs) => [...prevLogs, ...newLogs]); setLogsOffset((prevOffset) => prevOffset + newLogs.length); } setHasMoreLogs(newLogs.length === logsLimit); } catch (error) { console.error("Ошибка загрузки логов:", error); showNotification("Ошибка загрузки логов", "error"); } finally { setIsLoadingLogs(false); } }, [logTypeFilter, logsLimit, showNotification, logsOffset] ); const handleLogTypeFilterChange = (value: string) => { setLogTypeFilter(value); setLogsOffset(0); setHasMoreLogs(true); }; useEffect(() => { if (activeTab === "logs") { loadLogs(true); } }, [logTypeFilter, activeTab, loadLogs]); const formatLogAction = (actionType: string) => { const actionTypes: Record = { login: "Вход", logout: "Выход", register: "Регистрация", note_create: "Создание заметки", note_update: "Редактирование", note_delete: "Удаление", note_pin: "Закрепление", note_archive: "Архивирование", note_unarchive: "Восстановление", note_delete_permanent: "Окончательное удаление", profile_update: "Обновление профиля", ai_improve: "Улучшение через AI", }; return actionTypes[actionType] || actionType; }; return (
Настройки
{/* Табы навигации */}
{/* Контент табов */}
{/* Внешний вид */} {activeTab === "appearance" && (

Внешний вид

{colorOptions.map((option) => (
setSelectedAccentColor(option.color)} /> ))}
)} {/* AI настройки */} {activeTab === "ai" && (

Настройки AI

{ setApiKey(e.target.value); updateAiToggleState(); }} />

Введите ваш OpenAI API ключ

{ setBaseUrl(e.target.value); updateAiToggleState(); }} />

URL для API запросов (например, https://api.openai.com/v1)

{ setModel(e.target.value); updateAiToggleState(); }} />

Название модели (например, gpt-4, deepseek/deepseek-chat). {" "} Список доступных моделей

)} {/* Архив заметок */} {activeTab === "archive" && (

Архивные заметки

Архивированные заметки можно восстановить или удалить окончательно

{isLoadingArchived ? (

Загрузка...

) : archivedNotes.length === 0 ? (

Архив пуст

) : ( archivedNotes.map((note) => { const created = new Date( note.created_at.replace(" ", "T") + "Z" ); const dateStr = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(created); const htmlContent = parseMarkdown(note.content); const preview = htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : ""); return (
{dateStr}
{note.images && note.images.length > 0 && (
{note.images.length} изображений
)}
); }) )}
)} {/* История действий */} {activeTab === "logs" && (

История действий

{/* Фильтры */}
{/* Таблица логов */}
{isLoadingLogs && logs.length === 0 ? ( ) : logs.length === 0 ? ( ) : ( logs.map((log) => { const created = new Date( log.created_at.replace(" ", "T") + "Z" ); const dateStr = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", }).format(created); return ( ); }) )}
Дата и время Действие Детали
Загрузка...
Логов пока нет
{dateStr} {formatLogAction(log.action_type)} {log.details || "-"}
{hasMoreLogs && logs.length > 0 && (
)}
)}
{/* Модальное окно подтверждения удаления всех архивных заметок */} { setIsDeleteAllModalOpen(false); setDeleteAllPassword(""); }} onConfirm={handleDeleteAllArchived} title="Подтверждение удаления" message={ <>

⚠️ ВНИМАНИЕ: Это действие нельзя отменить!

Вы действительно хотите удалить ВСЕ архивные заметки? Все заметки и их изображения будут удалены навсегда.

setDeleteAllPassword(e.target.value)} onKeyPress={(e) => { if (e.key === "Enter" && !isDeletingAll) { handleDeleteAllArchived(); } }} />
} confirmText={isDeletingAll ? "Удаление..." : "Удалить все"} cancelText="Отмена" confirmType="danger" />
); }; export default SettingsPage;