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(() => {})