diff --git a/alfy-bot-frontend/src/features/task-timer/model/timer-store.ts b/alfy-bot-frontend/src/features/task-timer/model/timer-store.ts index 470e8ec..43b98ae 100644 --- a/alfy-bot-frontend/src/features/task-timer/model/timer-store.ts +++ b/alfy-bot-frontend/src/features/task-timer/model/timer-store.ts @@ -374,6 +374,8 @@ export const useTimerStore = defineStore('timer', () => { }) nextPhase() + startTimer() + syncToBackend() } function registerSWListener(): void { diff --git a/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue b/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue index cda388e..3e63946 100644 --- a/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue +++ b/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue @@ -121,14 +121,27 @@ > {{ formatPomodoro(task.pomodoroCompleted || 0) }}/{{ localPomodoroCount }} + + + {{ timerState === 'running' ? 'идёт' : 'пауза' }} + import { ref, computed, watch, nextTick } from 'vue' +import { storeToRefs } from 'pinia' import { useMediaQuery } from '@vueuse/core' import { X, @@ -552,6 +566,7 @@ import { Bell, Repeat, Play, + Pause, } from 'lucide-vue-next' import { Dialog, DialogContent, DialogClose } from '@/components/ui/dialog' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' @@ -578,7 +593,7 @@ import { getPriorityColor } from '../lib/priority' import { type DueDateUrgency, getDueDateUrgency } from '../lib/urgency' import { createChecklistItem, computeChecklistProgress } from '../lib/checklist' import { countFromDurationMinutes } from '../lib/duration' -import { toTimerTask, useTimerStore } from '@/features/task-timer' +import { getTaskTimerState, toTimerTask, useTimerStore } from '@/features/task-timer' const URGENCY_CLASSES: Record = { overdue: 'text-red-500', @@ -703,10 +718,30 @@ const localOnMissed = ref<'shift' | 'freeze'>('shift') // Timer const timerStore = useTimerStore() +const { activeTaskId, phase, isActive } = storeToRefs(timerStore) + +const timerState = computed(() => { + if (!props.task?.isPomodoroTask) return 'idle' as const + return getTaskTimerState(props.task.id, { + activeTaskId: activeTaskId.value, + phase: phase.value, + isActive: isActive.value, + }) +}) + +const timerActionLabel = computed(() => { + if (timerState.value === 'running') return 'Пауза помодоро' + if (timerState.value === 'paused') return 'Продолжить помодоро' + return 'Запустить помодоро' +}) -function handleStartTimer() { +function handleToggleTimer() { if (!props.task?.isPomodoroTask) return - timerStore.startTask(toTimerTask(props.task)) + if (timerState.value === 'idle') { + timerStore.startTask(toTimerTask(props.task)) + return + } + timerStore.toggleTimer() } // Project diff --git a/alfy-bot-frontend/tests/features/task-timer/timer-store-pomodoro.spec.ts b/alfy-bot-frontend/tests/features/task-timer/timer-store-pomodoro.spec.ts index 0a4cb39..b4a0bfb 100644 --- a/alfy-bot-frontend/tests/features/task-timer/timer-store-pomodoro.spec.ts +++ b/alfy-bot-frontend/tests/features/task-timer/timer-store-pomodoro.spec.ts @@ -32,6 +32,16 @@ describe('timer store — запись помодоро', () => { vi.mocked(api.delete).mockResolvedValue({ data: {} }) }) + it('startTask сразу запускает таймер', () => { + const timer = useTimerStore() + + timer.startTask(POMODORO_TASK) + + expect(timer.isActive).toBe(true) + expect(timer.phase).toBe(1) + expect(timer.timeBlock).toBeGreaterThan(0) + }) + it('завершение рабочей фазы делегирует инкремент в task-store', async () => { const timer = useTimerStore() const tasks = useTaskStore() diff --git a/alfy-bot-frontend/tests/features/tasks/ui/CurrentTaskWidget.spec.ts b/alfy-bot-frontend/tests/features/tasks/ui/CurrentTaskWidget.spec.ts index 808cbe1..3688f05 100644 --- a/alfy-bot-frontend/tests/features/tasks/ui/CurrentTaskWidget.spec.ts +++ b/alfy-bot-frontend/tests/features/tasks/ui/CurrentTaskWidget.spec.ts @@ -116,7 +116,6 @@ describe('currentTaskWidget', () => { const wrapper = mountWith([activeTask()]) const timer = useTimerStore() timer.startTask(toTimerTask(activeTask())) - timer.isActive = true await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('идёт') @@ -124,12 +123,11 @@ describe('currentTaskWidget', () => { expect(wrapper.find('button').exists()).toBe(false) }) - // startTask только взводит сессию, тикать она начинает отдельно — поэтому - // сразу после клика строка оказывается именно в этом состоянии. - it('взведённая, но не тикающая сессия показывается как пауза и сохраняет кнопку', async () => { + it('пауза сохраняет кнопку «Продолжить»', async () => { const wrapper = mountWith([activeTask()]) const timer = useTimerStore() timer.startTask(toTimerTask(activeTask())) + timer.pauseTimer() await wrapper.vm.$nextTick() expect(wrapper.text()).toContain('пауза') @@ -141,6 +139,7 @@ describe('currentTaskWidget', () => { const wrapper = mountWith([activeTask()]) const timer = useTimerStore() timer.startTask(toTimerTask(activeTask())) + timer.pauseTimer() await wrapper.vm.$nextTick() const resume = vi.spyOn(timer, 'toggleTimer').mockImplementation(() => {})