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 [syncing, setSyncing] = useState(false) const [stats, setStats] = useState({ steps: { value: 0, goal: 10000 }, floors: { value: 0, goal: 10 } }) const [bindings, setBindings] = useState({ steps_task_id: null, floors_task_id: null, steps_goal_task_id: null, steps_goal_subtask_id: null, floors_goal_task_id: null, floors_goal_subtask_id: null }) const [editedBindings, setEditedBindings] = useState(bindings) const [savingBindings, setSavingBindings] = useState(false) const [tasks, setTasks] = useState([]) const [loadingTasks, setLoadingTasks] = useState(false) const [stepsGoalSubtasks, setStepsGoalSubtasks] = useState([]) const [floorsGoalSubtasks, setFloorsGoalSubtasks] = useState([]) const oauthStatusRef = React.useRef(null) useEffect(() => { 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}`) } window.history.replaceState({}, '', window.location.pathname) } checkStatus() }, []) useEffect(() => { if (connected) { loadStats() loadTasks() } }, [connected]) useEffect(() => { if (!editedBindings.steps_goal_task_id) { setStepsGoalSubtasks([]) return } let cancelled = false authFetch(`/api/tasks/${editedBindings.steps_goal_task_id}`) .then((r) => r.ok ? r.json() : null) .then((data) => { if (!cancelled && data?.subtasks) { setStepsGoalSubtasks(data.subtasks.map((s) => ({ id: s.task.id, name: s.task.name }))) } else if (!cancelled) { setStepsGoalSubtasks([]) } }) .catch(() => { if (!cancelled) setStepsGoalSubtasks([]) }) return () => { cancelled = true } }, [editedBindings.steps_goal_task_id, authFetch]) useEffect(() => { if (!editedBindings.floors_goal_task_id) { setFloorsGoalSubtasks([]) return } let cancelled = false authFetch(`/api/tasks/${editedBindings.floors_goal_task_id}`) .then((r) => r.ok ? r.json() : null) .then((data) => { if (!cancelled && data?.subtasks) { setFloorsGoalSubtasks(data.subtasks.map((s) => ({ id: s.task.id, name: s.task.name }))) } else if (!cancelled) { setFloorsGoalSubtasks([]) } }) .catch(() => { if (!cancelled) setFloorsGoalSubtasks([]) }) return () => { cancelled = true } }, [editedBindings.floors_goal_task_id, authFetch]) 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.bindings) { const b = data.bindings const normalized = { steps_task_id: b.steps_task_id ?? null, floors_task_id: b.floors_task_id ?? null, steps_goal_task_id: b.steps_goal_task_id ?? null, steps_goal_subtask_id: b.steps_goal_subtask_id ?? null, floors_goal_task_id: b.floors_goal_task_id ?? null, floors_goal_subtask_id: b.floors_goal_subtask_id ?? null } setBindings(normalized) setEditedBindings(normalized) } 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() setStats({ steps: { value: data.steps?.value ?? 0, goal: data.steps?.goal ?? 10000 }, floors: { value: data.floors?.value ?? 0, goal: data.floors?.goal ?? 10 } }) } catch (error) { console.error('Error loading stats:', error) } } const loadTasks = async () => { try { setLoadingTasks(true) const response = await authFetch('/api/tasks') if (!response.ok) { throw new Error('Ошибка при загрузке задач') } const data = await response.json() setTasks(data || []) } catch (error) { console.error('Error loading tasks:', error) } finally { setLoadingTasks(false) } } 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: 10000 }, floors: { value: 0, goal: 10 } }) 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 handleSaveBindings = async () => { try { setSavingBindings(true) const response = await authFetch('/api/integrations/fitbit/bindings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editedBindings) }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || 'Ошибка при сохранении привязок') } setBindings(editedBindings) setToastMessage({ text: 'Привязки сохранены', type: 'success' }) } catch (error) { console.error('Error saving bindings:', error) setToastMessage({ text: error.message || 'Не удалось сохранить привязки', type: 'error' }) } finally { setSavingBindings(false) } } const getProgressTasks = () => { return tasks.filter(t => t.has_progression || t.progression_base != null) } const getParentTasks = () => { return tasks.filter(t => (t.subtasks_count ?? 0) > 0) } const getProgressPercent = (value, goal) => { if (!goal || goal === 0) return 0 return Math.min(100, (value / goal) * 100) } const getProgressColor = (value, goal) => { if (value >= goal) return 'text-green-600' if (value >= goal * 0.5) return 'text-blue-600' return 'text-gray-600' } if (isLoadingError && !loading) { return (
{message}
Ошибка подключения Fitbit
{oauthError}
Подключите свой Fitbit аккаунт для автоматической синхронизации шагов и этажей с вашими задачами.