6.14.0: Еженедельное подтверждение приоритетов
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m19s
This commit is contained in:
@@ -134,6 +134,10 @@ function AppContent() {
|
||||
// Ref для функции открытия модала добавления записи в CurrentWeek
|
||||
const currentWeekAddModalRef = useRef(null)
|
||||
|
||||
// Подтверждение приоритетов на текущей неделе (null = неизвестно, true/false = известно)
|
||||
const [prioritiesConfirmed, setPrioritiesConfirmed] = useState(null)
|
||||
const prioritiesOverlayPushedRef = useRef(false)
|
||||
|
||||
// Кеширование данных
|
||||
const [currentWeekData, setCurrentWeekData] = useState(null)
|
||||
const [fullStatisticsData, setFullStatisticsData] = useState(null)
|
||||
@@ -173,7 +177,22 @@ function AppContent() {
|
||||
|
||||
// Восстанавливаем последний выбранный таб после перезагрузки
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
|
||||
// Управление историей для оверлея приоритетов
|
||||
useEffect(() => {
|
||||
const overlayVisible = activeTab === 'current' && prioritiesConfirmed === false
|
||||
if (overlayVisible && !prioritiesOverlayPushedRef.current) {
|
||||
prioritiesOverlayPushedRef.current = true
|
||||
// Заменяем текущую запись { tab: 'current' } на { tab: 'tasks' },
|
||||
// затем добавляем запись оверлея. Так системная кнопка "Назад" вернёт на tasks.
|
||||
window.history.replaceState({ tab: 'tasks' }, '', '/')
|
||||
window.history.pushState({ tab: 'current', prioritiesOverlay: true }, '', '/')
|
||||
}
|
||||
if (!overlayVisible) {
|
||||
prioritiesOverlayPushedRef.current = false
|
||||
}
|
||||
}, [activeTab, prioritiesConfirmed])
|
||||
|
||||
// Переключение на экран прогрессии после успешной авторизации
|
||||
useEffect(() => {
|
||||
// Обновляем ref только после того, как authLoading стал false
|
||||
@@ -199,6 +218,7 @@ function AppContent() {
|
||||
setFullStatisticsData(null)
|
||||
setTasksData(null)
|
||||
setTodayEntriesData(null)
|
||||
setPrioritiesConfirmed(null)
|
||||
// Сбрасываем инициализацию табов, чтобы данные загрузились заново
|
||||
Object.keys(tabsInitializedRef.current).forEach(key => {
|
||||
tabsInitializedRef.current[key] = false
|
||||
@@ -506,6 +526,9 @@ function AppContent() {
|
||||
const wishes = jsonData?.wishes || []
|
||||
const pendingScoresByProject = jsonData?.pending_scores_by_project && typeof jsonData.pending_scores_by_project === 'object' ? jsonData.pending_scores_by_project : {}
|
||||
|
||||
const rootData = (Array.isArray(jsonData) && jsonData.length > 0) ? jsonData[0] : jsonData
|
||||
const prioritiesConfirmedValue = rootData?.priorities_confirmed ?? null
|
||||
|
||||
setCurrentWeekData({
|
||||
projects: Array.isArray(projects) ? projects : [],
|
||||
total: total,
|
||||
@@ -515,6 +538,10 @@ function AppContent() {
|
||||
wishes: wishes,
|
||||
pending_scores_by_project: pendingScoresByProject
|
||||
})
|
||||
|
||||
if (prioritiesConfirmedValue !== null) {
|
||||
setPrioritiesConfirmed(prioritiesConfirmedValue)
|
||||
}
|
||||
} catch (err) {
|
||||
setCurrentWeekError(err.message)
|
||||
console.error('Ошибка загрузки данных текущей недели:', err)
|
||||
@@ -796,7 +823,7 @@ function AppContent() {
|
||||
}
|
||||
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'dictionaries', 'test', 'purchase', 'tasks', 'task-form', 'wishlist', 'wishlist-form', 'wishlist-detail', 'profile', 'todoist-integration', 'telegram-integration', 'tracking', 'tracking-access', 'tracking-invite', 'shopping', 'shopping-item-form', 'shopping-board-form', 'shopping-board-join', 'shopping-item-history']
|
||||
|
||||
|
||||
// Проверяем state текущей записи истории (куда мы вернулись)
|
||||
if (event.state && event.state.tab) {
|
||||
const { tab, params = {} } = event.state
|
||||
@@ -1123,6 +1150,8 @@ function AppContent() {
|
||||
let paddingClasses = ''
|
||||
if (tabName === 'current' || tabName === 'tasks' || tabName === 'wishlist' || tabName === 'profile') {
|
||||
paddingClasses = 'pb-20'
|
||||
} else if (tabName === 'priorities') {
|
||||
paddingClasses = 'pb-20'
|
||||
} else if (tabName === 'words' || tabName === 'dictionaries' || tabName === 'shopping') {
|
||||
paddingClasses = 'pb-16'
|
||||
}
|
||||
@@ -1171,7 +1200,7 @@ function AppContent() {
|
||||
{loadedTabs.priorities && (
|
||||
<div className={getTabContainerClasses('priorities')}>
|
||||
<div className={getInnerContainerClasses('priorities')}>
|
||||
<ProjectPriorityManager
|
||||
<ProjectPriorityManager
|
||||
allProjectsData={fullStatisticsData}
|
||||
currentWeekData={currentWeekData}
|
||||
shouldLoad={activeTab === 'priorities' && loadedTabs.priorities}
|
||||
@@ -1179,6 +1208,12 @@ function AppContent() {
|
||||
onErrorChange={setPrioritiesError}
|
||||
refreshTrigger={prioritiesRefreshTrigger}
|
||||
onNavigate={handleNavigate}
|
||||
onConfirmed={async () => {
|
||||
await fetchCurrentWeekData(false)
|
||||
setPrioritiesConfirmed(true)
|
||||
markTabAsLoaded('current')
|
||||
setActiveTab('current')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1742,6 +1777,31 @@ function AppContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Оверлей подтверждения приоритетов — показывается поверх экрана прогресса недели */}
|
||||
{activeTab === 'current' && prioritiesConfirmed === false && (
|
||||
<div className="fixed inset-0 bg-white z-50 overflow-y-auto">
|
||||
<div className="max-w-2xl mx-auto px-4 h-full">
|
||||
<ProjectPriorityManager
|
||||
allProjectsData={fullStatisticsData}
|
||||
currentWeekData={currentWeekData}
|
||||
shouldLoad={true}
|
||||
onLoadingChange={setPrioritiesLoading}
|
||||
onErrorChange={setPrioritiesError}
|
||||
refreshTrigger={Math.max(prioritiesRefreshTrigger, 1)}
|
||||
onNavigate={handleNavigate}
|
||||
onConfirmed={async () => {
|
||||
await fetchCurrentWeekData(false)
|
||||
setPrioritiesConfirmed(true)
|
||||
}}
|
||||
onClose={() => {
|
||||
// history.back() переходит к { tab: 'tasks' }, popstate обработает переключение
|
||||
window.history.back()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ import './Integrations.css'
|
||||
|
||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||
const PROJECTS_API_URL = '/projects'
|
||||
const PRIORITY_UPDATE_API_URL = '/project/priority'
|
||||
const PROJECT_COLOR_API_URL = '/project/color'
|
||||
const PROJECT_MOVE_API_URL = '/project/move'
|
||||
const PROJECT_CREATE_API_URL = '/project/create'
|
||||
const PRIORITIES_CONFIRM_API_URL = '/priorities/confirm'
|
||||
|
||||
// Компонент экрана добавления проекта
|
||||
function AddProjectScreen({ onClose, onSuccess, onError }) {
|
||||
@@ -387,13 +387,14 @@ function PrioritySlot({ title, projects, allProjects, onMenuClick, maxItems = nu
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, onLoadingChange, onErrorChange, refreshTrigger, onNavigate }) {
|
||||
function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad, onLoadingChange, onErrorChange, refreshTrigger, onNavigate, onConfirmed, onClose }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [projectsLoading, setProjectsLoading] = useState(false)
|
||||
const [projectsError, setProjectsError] = useState(null)
|
||||
const [hasDataCache, setHasDataCache] = useState(false) // Отслеживаем наличие кеша
|
||||
const [toastMessage, setToastMessage] = useState(null)
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
// Уведомляем родительский компонент об изменении состояния загрузки
|
||||
useEffect(() => {
|
||||
if (onLoadingChange) {
|
||||
@@ -421,9 +422,8 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
|
||||
const scrollContainerRef = useRef(null)
|
||||
const hasFetchedRef = useRef(false)
|
||||
const skipNextEffectRef = useRef(false)
|
||||
const lastRefreshTriggerRef = useRef(0) // Отслеживаем последний обработанный refreshTrigger
|
||||
const isLoadingRef = useRef(false) // Отслеживаем, идет ли сейчас загрузка
|
||||
const lastRefreshTriggerRef = useRef(0)
|
||||
const isLoadingRef = useRef(false)
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -608,60 +608,30 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
return map
|
||||
}, [lowPriority, maxPriority, mediumPriority])
|
||||
|
||||
const prevAssignmentsRef = useRef(new Map())
|
||||
const initializedAssignmentsRef = useRef(false)
|
||||
const handleSave = useCallback(async () => {
|
||||
const assignments = buildAssignments()
|
||||
const changes = []
|
||||
assignments.forEach(({ id, priority }) => {
|
||||
if (id) changes.push({ id, priority })
|
||||
})
|
||||
|
||||
const sendPriorityChanges = useCallback(async (changes) => {
|
||||
if (!changes.length) return
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await authFetch(PRIORITY_UPDATE_API_URL, {
|
||||
const response = await authFetch(PRIORITIES_CONFIRM_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(changes),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Ошибка отправки изменений приоритета', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const current = buildAssignments()
|
||||
|
||||
if (!initializedAssignmentsRef.current) {
|
||||
prevAssignmentsRef.current = current
|
||||
initializedAssignmentsRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (skipNextEffectRef.current) {
|
||||
skipNextEffectRef.current = false
|
||||
prevAssignmentsRef.current = current
|
||||
return
|
||||
}
|
||||
|
||||
const prev = prevAssignmentsRef.current
|
||||
const allKeys = new Set([...prev.keys(), ...current.keys()])
|
||||
const changes = []
|
||||
|
||||
allKeys.forEach(key => {
|
||||
const prevItem = prev.get(key)
|
||||
const currItem = current.get(key)
|
||||
const prevPriority = prevItem?.priority ?? null
|
||||
const currPriority = currItem?.priority ?? null
|
||||
const id = currItem?.id ?? prevItem?.id
|
||||
|
||||
if (!id) return
|
||||
if (prevPriority !== currPriority) {
|
||||
changes.push({ id, priority: currPriority })
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка сохранения')
|
||||
}
|
||||
})
|
||||
|
||||
if (changes.length) {
|
||||
sendPriorityChanges(changes)
|
||||
if (onConfirmed) onConfirmed()
|
||||
} catch (e) {
|
||||
setToastMessage({ text: e.message || 'Ошибка сохранения', type: 'error' })
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
prevAssignmentsRef.current = current
|
||||
}, [buildAssignments, sendPriorityChanges])
|
||||
}, [authFetch, buildAssignments, onConfirmed])
|
||||
|
||||
const findProjectContainer = (projectName) => {
|
||||
if (maxPriority.find(p => p.name === projectName)) return 'max'
|
||||
@@ -919,9 +889,9 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto flex flex-col h-full">
|
||||
{onNavigate && (
|
||||
{(onNavigate || onClose) && (
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
onClick={() => onClose ? onClose() : window.history.back()}
|
||||
className="close-x-button"
|
||||
title="Закрыть"
|
||||
>
|
||||
@@ -1090,6 +1060,21 @@ function ProjectPriorityManager({ allProjectsData, currentWeekData, shouldLoad,
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={onClose
|
||||
? 'sticky bottom-0 pt-2 pb-4 mt-2'
|
||||
: 'fixed bottom-0 left-0 right-0 bg-gray-100 px-4 pt-2 pb-4'
|
||||
}>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="w-full py-3 bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-semibold rounded-lg shadow transition-all"
|
||||
>
|
||||
{isSaving ? 'Сохранение...' : 'Сохранить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{toastMessage && (
|
||||
<Toast
|
||||
message={toastMessage.text}
|
||||
|
||||
@@ -30,6 +30,8 @@ function getLastFiveWeeks() {
|
||||
return weeks.reverse() // От старой к новой
|
||||
}
|
||||
|
||||
// Проверяет, закончилась ли уже данная ISO-неделя (текущая неделя не закончилась)
|
||||
|
||||
function Tracking({ onNavigate, activeTab }) {
|
||||
const { authFetch } = useAuth()
|
||||
const [weeks, setWeeks] = useState(() => getLastFiveWeeks())
|
||||
@@ -147,7 +149,7 @@ function Tracking({ onNavigate, activeTab }) {
|
||||
) : (
|
||||
<div className="users-list">
|
||||
{data?.users.map(user => (
|
||||
<UserTrackingCard key={user.user_id} user={user} />
|
||||
<UserTrackingCard key={user.user_id} user={user} selectedWeek={selectedWeek} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -156,7 +158,7 @@ function Tracking({ onNavigate, activeTab }) {
|
||||
}
|
||||
|
||||
// Карточка пользователя с прогрессом
|
||||
function UserTrackingCard({ user }) {
|
||||
function UserTrackingCard({ user, selectedWeek }) {
|
||||
// Сортируем проекты по priority (1, 2, остальные)
|
||||
const sortedProjects = [...user.projects].sort((a, b) => {
|
||||
const pa = a.priority ?? 99
|
||||
@@ -169,10 +171,27 @@ function UserTrackingCard({ user }) {
|
||||
return percent >= 100 ? 'percent-green' : 'percent-blue'
|
||||
}
|
||||
|
||||
// Показываем (черновик) если выбранная неделя позже недели подтверждения
|
||||
const showDraft = selectedWeek && (() => {
|
||||
const cy = user.priorities_confirmed_year || 0
|
||||
const cw = user.priorities_confirmed_week || 0
|
||||
const sy = selectedWeek.year
|
||||
const sw = selectedWeek.week
|
||||
// Неделя не подтверждена вообще (0,0) или выбранная неделя позже подтверждённой
|
||||
return sy > cy || (sy === cy && sw > cw)
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className={`user-tracking-card ${user.is_current_user ? 'current-user' : ''}`}>
|
||||
<div className="user-header">
|
||||
<span className="user-name">{user.user_name}</span>
|
||||
<span className="user-name">
|
||||
{user.user_name}
|
||||
{showDraft && (
|
||||
<span style={{ color: '#9ca3af', fontWeight: 'normal', fontSize: '0.85em', marginLeft: '4px' }}>
|
||||
(черновик)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className={`user-total ${getPercentColorClass(totalPercent)}`}>{totalPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="projects-list">
|
||||
|
||||
Reference in New Issue
Block a user