import React, { useState, useEffect } from 'react' import { useAuth } from './auth/AuthContext' import LoadingError from './LoadingError' import Toast from './Toast' import './Integrations.css' function FitbitIntegration({ onNavigate }) { const { authFetch } = useAuth() const [connected, setConnected] = useState(false) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [message, setMessage] = useState('') const [oauthError, setOauthError] = useState('') const [toastMessage, setToastMessage] = useState(null) const [isLoadingError, setIsLoadingError] = useState(false) const [goals, setGoals] = useState({ steps: { min: 8000, max: 10000 }, floors: { min: 8, max: 10 }, azm: { min: 22, max: 44 } }) const [stats, setStats] = useState({ steps: { value: 0, goal: { min: 8000, max: 10000 } }, floors: { value: 0, goal: { min: 8, max: 10 } }, azm: { value: 0, goal: { min: 22, max: 44 } } }) const [isEditingGoals, setIsEditingGoals] = useState(false) const [editedGoals, setEditedGoals] = useState(goals) const [syncing, setSyncing] = useState(false) // Сохраняем OAuth статус из URL в ref, чтобы проверить после checkStatus const oauthStatusRef = React.useRef(null) useEffect(() => { // Проверяем URL параметры для сообщений ДО вызова checkStatus const params = new URLSearchParams(window.location.search) const integration = params.get('integration') const status = params.get('status') if (integration === 'fitbit') { oauthStatusRef.current = status if (status === 'connected') { setMessage('Fitbit успешно подключен!') } else if (status === 'error') { const errorMsg = params.get('message') || 'unknown_error' const errorMessages = { 'config_error': 'Ошибка конфигурации сервера. Обратитесь к администратору.', 'invalid_state': 'Недействительный токен авторизации. Попробуйте ещё раз.', 'no_code': 'Не получен код авторизации от Fitbit. Попробуйте ещё раз.', 'token_exchange_failed': 'Не удалось обменять код на токен. Проверьте настройки Fitbit приложения.', 'user_info_failed': 'Не удалось получить информацию о пользователе Fitbit.', 'db_error': 'Ошибка сохранения данных. Попробуйте ещё раз.', 'unknown_error': 'Произошла неизвестная ошибка при подключении Fitbit.' } setOauthError(errorMessages[errorMsg] || `Ошибка: ${errorMsg}`) } // Очищаем URL параметры window.history.replaceState({}, '', window.location.pathname) } checkStatus() }, []) useEffect(() => { if (connected) { loadStats() } }, [connected]) const checkStatus = async () => { try { setLoading(true) setError('') const response = await authFetch('/api/integrations/fitbit/status') if (!response.ok) { throw new Error('Ошибка при проверке статуса') } const data = await response.json() setConnected(data.connected || false) if (data.connected && data.goals) { setGoals(data.goals) setEditedGoals(data.goals) } // Если OAuth вернул status=connected, но бэкенд не подтвердил подключение if (oauthStatusRef.current === 'connected' && !data.connected) { setOauthError('Авторизация в Fitbit прошла, но подключение не сохранилось. Попробуйте ещё раз или обратитесь к администратору.') setMessage('') } oauthStatusRef.current = null } catch (error) { console.error('Error checking status:', error) setError(error.message || 'Не удалось проверить статус') setIsLoadingError(true) } finally { setLoading(false) } } const loadStats = async () => { try { const response = await authFetch('/api/integrations/fitbit/stats') if (!response.ok) { throw new Error('Ошибка при загрузке статистики') } const data = await response.json() // Нормализуем данные, чтобы избежать undefined const defaultGoal = { min: 0, max: 0 } const normalizedStats = { steps: { value: data.steps?.value ?? 0, goal: data.steps?.goal ?? defaultGoal }, floors: { value: data.floors?.value ?? 0, goal: data.floors?.goal ?? defaultGoal }, azm: { value: data.azm?.value ?? 0, goal: data.azm?.goal ?? defaultGoal } } setStats(normalizedStats) // Обновляем цели из ответа if (data.steps?.goal) { setGoals({ steps: data.steps.goal, floors: data.floors?.goal ?? defaultGoal, azm: data.azm?.goal ?? defaultGoal }) setEditedGoals({ steps: data.steps.goal, floors: data.floors?.goal ?? defaultGoal, azm: data.azm?.goal ?? defaultGoal }) } } catch (error) { console.error('Error loading stats:', error) // Не показываем ошибку, просто не обновляем статистику } } const handleConnect = async () => { try { setLoading(true) setError('') const response = await authFetch('/api/integrations/fitbit/oauth/connect') if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || 'Ошибка при подключении Fitbit') } const data = await response.json() if (data.auth_url) { window.location.href = data.auth_url } else { throw new Error('URL для авторизации не получен') } } catch (error) { console.error('Error connecting Fitbit:', error) setToastMessage({ text: error.message || 'Не удалось подключить Fitbit', type: 'error' }) setLoading(false) } } const handleDisconnect = async () => { if (!window.confirm('Вы уверены, что хотите отключить Fitbit?')) { return } try { setLoading(true) setError('') const response = await authFetch('/api/integrations/fitbit/disconnect', { method: 'DELETE', }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || 'Ошибка при отключении') } setConnected(false) setStats({ steps: { value: 0, goal: { min: 8000, max: 10000 } }, floors: { value: 0, goal: { min: 8, max: 10 } }, azm: { value: 0, goal: { min: 22, max: 44 } } }) setToastMessage({ text: 'Fitbit отключен', type: 'success' }) } catch (error) { console.error('Error disconnecting:', error) setToastMessage({ text: error.message || 'Не удалось отключить Fitbit', type: 'error' }) } finally { setLoading(false) } } const handleSync = async () => { try { setSyncing(true) const response = await authFetch('/api/integrations/fitbit/sync', { method: 'POST', }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || 'Ошибка при синхронизации') } setToastMessage({ text: 'Данные синхронизированы', type: 'success' }) await loadStats() } catch (error) { console.error('Error syncing:', error) setToastMessage({ text: error.message || 'Не удалось синхронизировать данные', type: 'error' }) } finally { setSyncing(false) } } const handleSaveGoals = async () => { try { const response = await authFetch('/api/integrations/fitbit/goals', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ steps: editedGoals.steps, floors: editedGoals.floors, azm: editedGoals.azm, }), }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || 'Ошибка при сохранении целей') } setGoals(editedGoals) setIsEditingGoals(false) setToastMessage({ text: 'Цели сохранены', type: 'success' }) await loadStats() } catch (error) { console.error('Error saving goals:', error) setToastMessage({ text: error.message || 'Не удалось сохранить цели', type: 'error' }) } } const handleCancelEdit = () => { setEditedGoals(goals) setIsEditingGoals(false) } const getProgressPercent = (value, min, max) => { if (value >= max) return 100 if (value <= min) return (value / min) * 50 return 50 + ((value - min) / (max - min)) * 50 } const getProgressColor = (value, min, max) => { if (value >= max) return 'text-green-600' if (value >= min) return 'text-blue-600' return 'text-gray-600' } if (isLoadingError && !loading) { return (
{message}
{oauthError}
✅ Fitbit подключен! Данные синхронизируются автоматически каждые 4 часа.
Вы также можете синхронизировать данные вручную, нажав кнопку "Синхронизировать".
Ошибка подключения Fitbit
{oauthError}
Подключите свой Fitbit аккаунт для отслеживания шагов, этажей и баллов кардионагрузки.