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
)}
{/* Line Chart for Daily Averages */}
{viewMode !== 'day' && lineChartData.length > 0 && (
)}
>
)}
);
};
if (loading) {
return (
);
}
if (error) {
return (
);
}
return (
)}
{/* Category Management Modal */}
{showCategoryModal && (
)}
);
};
export default App;
Нет данных для статистики.
; } // 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 => (
))}
{/* Radar Chart */}
{viewMode !== 'day' && radarChartData.length > 0 && (
{getIconComponent(cat.icon)}
{cat.name}:
{categoryPeriodAverages[cat.name]}
Баланс по категориям (Радарная диаграмма):
Динамика среднего балла за период:
Загрузка приложения...
Ошибка: {error}
{/* Header */}
{/* Main Content */}
{/* Daily Entry Modal */}
{showEntryModal && (
Мой Трекер Продуктивности
{/* User ID display removed */}
{/* Control Buttons */}
{/* Calendar View */}
{viewMode === 'month' && (
)}
{/* Statistics View */}
{viewMode !== 'month' && renderStatistics()}
{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)}
)}
);
})}
Оценка за {selectedDate}
{categories.map(cat => (
{cat.description && (
))}
{/* Working Hours for "Работа" */}
{categories.some(cat => cat.name === "Работа" && cat.type === "work") && (
{cat.description}
)}
{[1, 2, 3, 4].map(score => (
))}
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 */}
Управление категориями
Добавить новую категорию:
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}
))}