Выбрать страницу
import React, { useState, useEffect, useCallback } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, doc, getDoc, setDoc, updateDoc, onSnapshot, collection, query, addDoc, deleteDoc, getDocs } from 'firebase/firestore'; import { CalendarDays, Plus, X, Settings, BarChart, Sun, Moon, Briefcase, Heart, Users, BookOpen, Brush, Lightbulb, DollarSign, TrendingUp, ChevronLeft, ChevronRight, Brain, Dumbbell, Utensils, Coffee, Newspaper } from 'lucide-react'; import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; // Tailwind CSS is assumed to be available // Global variables for Firebase configuration (provided by the environment) const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? initialAuthToken : null; // Initialize Firebase outside of the component to avoid re-initialization let app, db, auth; if (Object.keys(firebaseConfig).length > 0) { app = initializeApp(firebaseConfig); db = getFirestore(app); auth = getAuth(app); } else { console.error("Firebase configuration is missing. Please ensure __firebase_config is provided."); } // Helper function to get the start of the day in UTC to avoid timezone issues const getStartOfDay = (date) => { const d = new Date(date); d.setUTCHours(0, 0, 0, 0); return d.toISOString().split('T')[0]; // YYYY-MM-DD format }; // Helper function to get a unique ID for a document const generateUniqueId = () => { return Date.now().toString(36) + Math.random().toString(36).substring(2); }; const App = () => { const [userId, setUserId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [categories, setCategories] = useState([]); const [dailyEntries, setDailyEntries] = useState({}); // { 'YYYY-MM-DD': { category: score, ... } } const [selectedDate, setSelectedDate] = useState(getStartOfDay(new Date())); const [currentMonth, setCurrentMonth] = useState(new Date()); // For calendar navigation const [showEntryModal, setShowEntryModal] = useState(false); const [showCategoryModal, setShowCategoryModal] = useState(false); const [newCategoryName, setNewCategoryName] = useState(''); const [currentEntry, setCurrentEntry] = useState({}); // For the form inputs const [dailyNote, setDailyNote] = useState(''); const [workingHours, setWorkingHours] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState('month'); // 'day', 'week', 'month', 'year' // --- Firebase Authentication and Initialization --- useEffect(() => { if (!app) { setError("Firebase is not initialized. Check configuration."); setLoading(false); return; } const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { setUserId(user.uid); } else { try { if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); } else { await signInAnonymously(auth); } setUserId(auth.currentUser.uid); } catch (e) { console.error("Error signing in:", e); setError("Ошибка входа в систему."); } } setIsAuthReady(true); setLoading(false); }); return () => unsubscribe(); }, []); // --- Fetch Categories and Daily Entries --- useEffect(() => { if (!isAuthReady || !userId || !db) return; // Fetch Categories const categoriesColRef = collection(db, `artifacts/${appId}/users/${userId}/categories`); const unsubscribeCategories = onSnapshot(categoriesColRef, (snapshot) => { const fetchedCategories = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); if (fetchedCategories.length === 0) { // If no categories, add default ones addDefaultCategories(userId); } else { setCategories(fetchedCategories.sort((a, b) => a.order - b.order)); } }, (err) => { console.error("Error fetching categories:", err); setError("Ошибка загрузки категорий."); }); // Fetch Daily Entries const entriesColRef = collection(db, `artifacts/${appId}/users/${userId}/dailyEntries`); const unsubscribeEntries = onSnapshot(entriesColRef, (snapshot) => { const fetchedEntries = {}; snapshot.docs.forEach(doc => { fetchedEntries[doc.id] = doc.data(); // doc.id is the YYYY-MM-DD date string }); setDailyEntries(fetchedEntries); }, (err) => { console.error("Error fetching daily entries:", err); setError("Ошибка загрузки ежедневных записей."); }); return () => { unsubscribeCategories(); unsubscribeEntries(); }; }, [isAuthReady, userId, db]); // --- Default Categories --- const addDefaultCategories = async (uid) => { if (!db || !uid) return; const defaultCats = [ { name: "Работа", icon: "Briefcase", order: 1, type: "work", description: "Насколько продуктивно вы работали сегодня? (1-4)" }, { name: "Развитие", icon: "BookOpen", order: 2, description: "Насколько вы продвинулись в личном или профессиональном развитии? (1-4)" }, { name: "Нетворкинг", icon: "Users", order: 3, description: "Насколько активно вы взаимодействовали с новыми людьми или поддерживали связи? (1-4)" }, { name: "Ментальное здоровье", icon: "Brain", order: 4, description: "Как вы оцениваете свое ментальное состояние сегодня? (1-4)" }, { name: "Физическое здоровье", icon: "Heart", order: 5, description: "Как вы оцениваете свое физическое самочувствие сегодня? (1-4)" }, { name: "Спорт", icon: "Dumbbell", order: 6, description: "Насколько вы были активны физически? (1-4)" }, { name: "Питание", icon: "Utensils", order: 7, description: "Насколько хорошо вы придерживались здорового питания? (1-4)" }, { name: "Отдых/Досуг", icon: "Coffee", order: 8, description: "Насколько качественно вы отдохнули и провели досуг? (1-4)" }, { name: "Изучение новостей", icon: "Newspaper", order: 9, description: "Насколько полезные новости вы изучили и сделали выводы? (1-4)" }, { name: "Планы", icon: "Lightbulb", order: 10, description: "Насколько хорошо вы следовали своим планам и целям? (1-4)" }, { name: "Семья", icon: "Users", order: 11, description: "Сколько качественного времени вы провели с семьей? (1-4)" }, { name: "Хобби", icon: "Brush", order: 12, description: "Насколько вы были вовлечены в свои хобби и творчество? (1-4)" }, { name: "Сон", icon: "Moon", order: 13, description: "Как вы оцениваете качество своего сна? (1-4)" } ]; const batch = []; for (const cat of defaultCats) { const docRef = doc(collection(db, `artifacts/${appId}/users/${uid}/categories`), generateUniqueId()); batch.push(setDoc(docRef, cat)); } try { await Promise.all(batch); console.log("Default categories added."); } catch (e) { console.error("Error adding default categories:", e); setError("Ошибка добавления категорий по умолчанию."); } }; // --- Calendar Navigation --- const changeMonth = (offset) => { setCurrentMonth(prev => { const newMonth = new Date(prev); newMonth.setMonth(prev.getMonth() + offset); return newMonth; }); }; // --- Daily Entry Logic --- const handleDayClick = (dateString) => { setSelectedDate(dateString); const entry = dailyEntries[dateString]; if (entry) { setCurrentEntry(entry.scores || {}); setDailyNote(entry.note || ''); setWorkingHours(entry.workingHours || ''); } else { setCurrentEntry({}); setDailyNote(''); setWorkingHours(''); } setShowEntryModal(true); }; const handleScoreChange = (categoryName, score) => { setCurrentEntry(prev => ({ ...prev, [categoryName]: score })); }; const saveDailyEntry = async () => { if (!userId || !db) { setError("Пользователь не авторизован или база данных недоступна."); return; } const entryRef = doc(db, `artifacts/${appId}/users/${userId}/dailyEntries`, selectedDate); const dataToSave = { date: selectedDate, scores: currentEntry, note: dailyNote, workingHours: workingHours, timestamp: new Date().toISOString() }; try { await setDoc(entryRef, dataToSave, { merge: true }); console.log("Daily entry saved for", selectedDate); setShowEntryModal(false); } catch (e) { console.error("Error saving daily entry:", e); setError("Ошибка сохранения записи."); } }; // --- Category Management Logic --- const addCategory = async () => { if (!newCategoryName.trim() || !userId || !db) return; const newCat = { name: newCategoryName.trim(), icon: "Lightbulb", // Default icon, can be improved later order: categories.length + 1, description: `Как вы оцениваете "${newCategoryName.trim()}" сегодня? (1-4)` }; try { await addDoc(collection(db, `artifacts/${appId}/users/${userId}/categories`), newCat); setNewCategoryName(''); } catch (e) { console.error("Error adding category:", e); setError("Ошибка добавления категории."); } }; const deleteCategory = async (id) => { if (!userId || !db) return; try { await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/categories`, id)); } catch (e) { console.error("Error deleting category:", e); setError("Ошибка удаления категории."); } }; // --- Statistics Calculation --- const calculateDailyAverage = useCallback((dateString) => { const entry = dailyEntries[dateString]; if (!entry || !entry.scores) return 0; const scores = Object.values(entry.scores).filter(score => typeof score === 'number' && score > 0); if (scores.length === 0) return 0; return scores.reduce((sum, score) => sum + score, 0) / scores.length; }, [dailyEntries]); const getMonthData = useCallback(() => { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const firstDayOfMonth = new Date(year, month, 1); const lastDayOfMonth = new Date(year, month + 1, 0); const daysInMonth = lastDayOfMonth.getDate(); const calendarDays = []; // Add leading empty days for the start of the week const startDayOfWeek = firstDayOfMonth.getDay(); // 0 for Sunday, 1 for Monday const offset = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1; // Adjust for Monday start for (let i = 0; i < offset; i++) { calendarDays.push(null); } for (let i = 1; i <= daysInMonth; i++) { const date = new Date(year, month, i); calendarDays.push(getStartOfDay(date)); } return calendarDays; }, [currentMonth]); const getScoreColor = (score) => { if (score === 0) return 'bg-gray-200'; if (score > 3.5) return 'bg-green-400'; if (score > 2.5) return 'bg-yellow-400'; if (score > 1.5) return 'bg-orange-400'; return 'bg-red-400'; }; const getIconComponent = (iconName) => { switch (iconName) { case 'CalendarDays': return ; case 'Plus': return ; case 'X': return ; case 'Settings': return ; case 'BarChart': return ; case 'Sun': return ; case 'Moon': return ; case 'Briefcase': return ; case 'Heart': return ; // Физическое здоровье case 'Brain': return ; // Ментальное здоровье case 'Dumbbell': return ; // Спорт case 'Utensils': return ; // Питание case 'Coffee': return ; // Отдых/Досуг (или Relax, but Coffee is available in Lucide) case 'Newspaper': return ; // Изучение новостей case 'Users': return ; case 'BookOpen': return ; case 'Brush': return ; case 'Lightbulb': return ; case 'DollarSign': return ; case 'TrendingUp': return ; default: return ; // Default icon } }; // --- Render Statistics based on ViewMode --- const renderStatistics = () => { if (Object.keys(dailyEntries).length === 0) { return

Нет данных для статистики.

; } // Helper to filter entries for a given period const filterEntriesByPeriod = (startDate, endDate) => { return Object.entries(dailyEntries).filter(([dateString, entry]) => { const entryDate = new Date(dateString); return entryDate >= startDate && entryDate <= endDate; }); }; // Calculate average scores for a given set of entries const calculateAverages = (entries) => { const categorySums = {}; const categoryCounts = {}; let totalOverallScore = 0; let totalOverallCount = 0; entries.forEach(([dateString, entry]) => { if (entry.scores) { Object.entries(entry.scores).forEach(([categoryName, score]) => { if (typeof score === 'number' && score > 0) { categorySums[categoryName] = (categorySums[categoryName] || 0) + score; categoryCounts[categoryName] = (categoryCounts[categoryName] || 0) + 1; totalOverallScore += score; totalOverallCount++; } }); } }); const averages = {}; for (const cat of categories) { averages[cat.name] = categoryCounts[cat.name] ? (categorySums[cat.name] / categoryCounts[cat.name]).toFixed(2) : 'N/A'; } const overallAverage = totalOverallCount ? (totalOverallScore / totalOverallCount).toFixed(2) : 'N/A'; return { averages, overallAverage }; }; let periodEntries = []; let periodTitle = ''; let overallPeriodAverage = 'N/A'; let categoryPeriodAverages = {}; let lineChartData = []; // Data for line chart let radarChartData = []; // Data for radar chart switch (viewMode) { case 'day': periodTitle = `Статистика за ${selectedDate}`; periodEntries = dailyEntries[selectedDate] ? [[selectedDate, dailyEntries[selectedDate]]] : []; break; case 'week': const selectedDateObj = new Date(selectedDate); const startOfWeek = new Date(selectedDateObj); startOfWeek.setDate(selectedDateObj.getDate() - (selectedDateObj.getDay() === 0 ? 6 : selectedDateObj.getDay() - 1)); // Monday start startOfWeek.setUTCHours(0, 0, 0, 0); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(startOfWeek.getDate() + 6); endOfWeek.setUTCHours(23, 59, 59, 999); periodTitle = `Статистика за неделю (${getStartOfDay(startOfWeek)} - ${getStartOfDay(endOfWeek)})`; periodEntries = filterEntriesByPeriod(startOfWeek, endOfWeek); break; case 'month': const startOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); startOfMonth.setUTCHours(0, 0, 0, 0); const endOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0); endOfMonth.setUTCHours(23, 59, 59, 999); periodTitle = `Статистика за ${currentMonth.toLocaleString('ru-RU', { month: 'long', year: 'numeric' })}`; periodEntries = filterEntriesByPeriod(startOfMonth, endOfMonth); break; case 'year': const startOfYear = new Date(currentMonth.getFullYear(), 0, 1); startOfYear.setUTCHours(0, 0, 0, 0); const endOfYear = new Date(currentMonth.getFullYear(), 11, 31); endOfYear.setUTCHours(23, 59, 59, 999); periodTitle = `Статистика за ${currentMonth.getFullYear()} год`; periodEntries = filterEntriesByPeriod(startOfYear, endOfYear); break; default: break; } const { averages, overallAverage } = calculateAverages(periodEntries); overallPeriodAverage = overallAverage; categoryPeriodAverages = averages; // Prepare data for Radar Chart radarChartData = categories.map(cat => ({ category: cat.name, score: parseFloat(categoryPeriodAverages[cat.name] || 0) })); // Prepare data for Line Chart (Daily Averages over the period) const sortedEntries = periodEntries.sort((a, b) => new Date(a[0]) - new Date(b[0])); lineChartData = sortedEntries.map(([dateString, entry]) => ({ date: dateString, average: calculateDailyAverage(dateString) })); return (

{periodTitle}

{periodEntries.length === 0 ? (

Нет данных за выбранный период.

) : ( <>

Общая средняя оценка: {overallPeriodAverage}

Средние оценки по категориям:

{categories.map(cat => (
{getIconComponent(cat.icon)} {cat.name}:
{categoryPeriodAverages[cat.name]}
))}
{/* Radar Chart */} {viewMode !== 'day' && radarChartData.length > 0 && (

Баланс по категориям (Радарная диаграмма):

)} {/* Line Chart for Daily Averages */} {viewMode !== 'day' && lineChartData.length > 0 && (

Динамика среднего балла за период:

)} )}
); }; if (loading) { return (
Загрузка приложения...
); } if (error) { return (

Ошибка: {error}

); } return (
{/* Header */}

Мой Трекер Продуктивности

{/* User ID display removed */}
{/* Main Content */}
{/* Control Buttons */}
{/* Calendar View */} {viewMode === 'month' && (

{currentMonth.toLocaleString('ru-RU', { month: 'long', year: 'numeric' })}

Пн
Вт
Ср
Чт
Пт
Сб
Вс
{getMonthData().map((dateString, index) => { const day = dateString ? new Date(dateString).getDate() : ''; const avgScore = dateString ? calculateDailyAverage(dateString) : 0; const isToday = dateString === getStartOfDay(new Date()); const hasEntry = dailyEntries[dateString]; return (
dateString && handleDayClick(dateString)} > {day} {hasEntry && avgScore > 0 && ( {avgScore.toFixed(1)} )}
); })}
)} {/* Statistics View */} {viewMode !== 'month' && renderStatistics()}
{/* Daily Entry Modal */} {showEntryModal && (

Оценка за {selectedDate}

{categories.map(cat => (
{cat.description && (

{cat.description}

)}
{[1, 2, 3, 4].map(score => ( ))}
))} {/* Working Hours for "Работа" */} {categories.some(cat => cat.name === "Работа" && cat.type === "work") && (
setWorkingHours(e.target.value)} className="w-full p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Например, 8" />
)} {/* Daily Note */}
)} {/* Category Management Modal */} {showCategoryModal && (

Управление категориями

Добавить новую категорию:

setNewCategoryName(e.target.value)} className="flex-grow p-3 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Название категории" />

Существующие категории:

    {categories.map(cat => (
  • {getIconComponent(cat.icon)} {cat.name}
  • ))}
)}
); }; export default App;