From e1465b5907f323287683c5162cc6b9facae85ff1 Mon Sep 17 00:00:00 2001 From: "i.seliverstov" Date: Sun, 16 Aug 2026 09:35:57 +0500 Subject: [PATCH] feat(tasks): add pomodoro to an existing task from the dialog The detail switch PUT/DELETEs pomodoro-config. Round count is chosen so the computed total is closest to the task's durationMinutes. Co-authored-by: Cursor --- .../src/features/tasks/lib/duration.spec.ts | 16 +++++++ .../src/features/tasks/lib/duration.ts | 42 +++++++++++++++---- .../tasks/lib/use-task-detail-handlers.ts | 12 ++++-- .../features/tasks/ui/TaskDetailDialog.vue | 35 ++++++++++++++-- .../lib/use-task-detail-handlers.spec.ts | 25 +++++++++++ .../src/modules/task/task.service.spec.ts | 16 +++++++ 6 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 alfy-bot-frontend/src/features/tasks/lib/duration.spec.ts diff --git a/alfy-bot-frontend/src/features/tasks/lib/duration.spec.ts b/alfy-bot-frontend/src/features/tasks/lib/duration.spec.ts new file mode 100644 index 0000000..c04b51a --- /dev/null +++ b/alfy-bot-frontend/src/features/tasks/lib/duration.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' +import { countFromDurationMinutes, computePomodoroTotalMinutes } from './duration' + +describe('countFromDurationMinutes', () => { + it('без duration → 4 раунда (дефолт)', () => { + expect(countFromDurationMinutes(computePomodoroTotalMinutes(4))).toBe(4) + }) + + it('60 минут → 2×25 (55 ближе, чем 1×25 или 3×25)', () => { + expect(countFromDurationMinutes(60)).toBe(2) + }) + + it('25 минут → 1 раунд', () => { + expect(countFromDurationMinutes(25)).toBe(1) + }) +}) diff --git a/alfy-bot-frontend/src/features/tasks/lib/duration.ts b/alfy-bot-frontend/src/features/tasks/lib/duration.ts index 2d21e38..f76fd01 100644 --- a/alfy-bot-frontend/src/features/tasks/lib/duration.ts +++ b/alfy-bot-frontend/src/features/tasks/lib/duration.ts @@ -1,18 +1,44 @@ import type { Task } from '../model/types' import { POMODORO_DEFAULTS } from '../model/constants' -export function computeTaskDurationMinutes(task: Task): number { - if (!task.isPomodoroTask) return task.durationMinutes ?? 60 - - const count = task.pomodoroCount ?? POMODORO_DEFAULTS.count - const duration = task.pomodoroDuration ?? POMODORO_DEFAULTS.duration - const shortBreak = task.shortBreak ?? POMODORO_DEFAULTS.shortBreak - const longBreak = task.longBreak ?? POMODORO_DEFAULTS.longBreak - const interval = task.longBreakInterval ?? POMODORO_DEFAULTS.longBreakInterval +const MAX_POMODORO_COUNT = 10 +export function computePomodoroTotalMinutes( + count: number, + duration = POMODORO_DEFAULTS.duration, + shortBreak = POMODORO_DEFAULTS.shortBreak, + longBreak = POMODORO_DEFAULTS.longBreak, + interval = POMODORO_DEFAULTS.longBreakInterval, +): number { let total = count * duration for (let i = 1; i < count; i++) { total += (i % interval === 0) ? longBreak : shortBreak } return total } + +export function computeTaskDurationMinutes(task: Task): number { + if (!task.isPomodoroTask) + return task.durationMinutes ?? 60 + + return computePomodoroTotalMinutes( + task.pomodoroCount ?? POMODORO_DEFAULTS.count, + task.pomodoroDuration ?? POMODORO_DEFAULTS.duration, + task.shortBreak ?? POMODORO_DEFAULTS.shortBreak, + task.longBreak ?? POMODORO_DEFAULTS.longBreak, + task.longBreakInterval ?? POMODORO_DEFAULTS.longBreakInterval, + ) +} + +export function countFromDurationMinutes(total: number): number { + let best = 1 + let bestDiff = Infinity + for (let count = 1; count <= MAX_POMODORO_COUNT; count++) { + const diff = Math.abs(computePomodoroTotalMinutes(count) - total) + if (diff < bestDiff) { + bestDiff = diff + best = count + } + } + return best +} diff --git a/alfy-bot-frontend/src/features/tasks/lib/use-task-detail-handlers.ts b/alfy-bot-frontend/src/features/tasks/lib/use-task-detail-handlers.ts index 2db171f..a7b4be3 100644 --- a/alfy-bot-frontend/src/features/tasks/lib/use-task-detail-handlers.ts +++ b/alfy-bot-frontend/src/features/tasks/lib/use-task-detail-handlers.ts @@ -143,20 +143,24 @@ export function useTaskDetailHandlers(store: TaskStore, confirm: ConfirmFn) { } } - async function handleUpdatePomodoroConfig(taskId: string, config: Record) { + async function handleUpdatePomodoroConfig(taskId: string, config: Record | null) { const index = tasks.value.findIndex(t => t.id === taskId) const previous: Task | null = index !== -1 ? { ...tasks.value[index] } as Task : null + const patch: Partial = config === null + ? { isPomodoroTask: false } + : { ...config, isPomodoroTask: true } if (index !== -1) { - tasks.value[index] = { ...tasks.value[index], ...config } as Task + tasks.value[index] = { ...tasks.value[index], ...patch } as Task } if (selectedTask.value?.id === taskId) { - selectedTask.value = { ...selectedTask.value, ...config } as Task + selectedTask.value = { ...selectedTask.value, ...patch } as Task } try { await store.updatePomodoroConfig(taskId, config) - } catch { + } + catch { if (previous && index !== -1) { tasks.value[index] = previous selectedTask.value = previous diff --git a/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue b/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue index b7251e3..1f6498d 100644 --- a/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue +++ b/alfy-bot-frontend/src/features/tasks/ui/TaskDetailDialog.vue @@ -108,16 +108,21 @@ -
+
Помодоро - + + {{ formatPomodoro(task.pomodoroCompleted || 0) }}/{{ localPomodoroCount }} +
= { @@ -625,7 +638,7 @@ const emit = defineEmits<{ shortBreak: number longBreak: number longBreakInterval: number - }): void + } | null): void }>() // Contenteditable refs @@ -821,6 +834,22 @@ function emitPomodoroUpdate() { }) } +function onPomodoroToggle(enabled: boolean) { + if (!props.task) return + if (!enabled) { + emit('update:pomodoroConfig', props.task.id, null) + return + } + localPomodoroCount.value = props.task.durationMinutes + ? countFromDurationMinutes(props.task.durationMinutes) + : POMODORO_DEFAULTS.count + localPomodoroDuration.value = POMODORO_DEFAULTS.duration + localShortBreak.value = POMODORO_DEFAULTS.shortBreak + localLongBreak.value = POMODORO_DEFAULTS.longBreak + localLongBreakInterval.value = POMODORO_DEFAULTS.longBreakInterval + emitPomodoroUpdate() +} + // Title function commitTitle() { if (props.task && localTitle.value !== props.task.title) { diff --git a/alfy-bot-frontend/tests/features/tasks/lib/use-task-detail-handlers.spec.ts b/alfy-bot-frontend/tests/features/tasks/lib/use-task-detail-handlers.spec.ts index 4986943..3cc8749 100644 --- a/alfy-bot-frontend/tests/features/tasks/lib/use-task-detail-handlers.spec.ts +++ b/alfy-bot-frontend/tests/features/tasks/lib/use-task-detail-handlers.spec.ts @@ -118,4 +118,29 @@ describe('useTaskDetailHandlers', () => { expect(isDetailOpen.value).toBe(false) expect(store.deleteTask).not.toHaveBeenCalled() }) + + it('handleUpdatePomodoroConfig ставит isPomodoroTask на selectedTask', async () => { + const task = makeTask({ id: '1' }) + store = createMockStore([task]) + const { handleOpenTask, handleUpdatePomodoroConfig, selectedTask } = useTaskDetailHandlers(store as any, confirmFn) + handleOpenTask(task) + + await handleUpdatePomodoroConfig('1', { pomodoroCount: 2, pomodoroDuration: 25 }) + + expect(selectedTask.value!.isPomodoroTask).toBe(true) + expect(selectedTask.value!.pomodoroCount).toBe(2) + expect(store.updatePomodoroConfig).toHaveBeenCalledWith('1', { pomodoroCount: 2, pomodoroDuration: 25 }) + }) + + it('handleUpdatePomodoroConfig с null снимает isPomodoroTask', async () => { + const task = makeTask({ id: '1', isPomodoroTask: true, pomodoroCount: 4 }) + store = createMockStore([task]) + const { handleOpenTask, handleUpdatePomodoroConfig, selectedTask } = useTaskDetailHandlers(store as any, confirmFn) + handleOpenTask(task) + + await handleUpdatePomodoroConfig('1', null) + + expect(selectedTask.value!.isPomodoroTask).toBe(false) + expect(store.updatePomodoroConfig).toHaveBeenCalledWith('1', null) + }) }) diff --git a/alfy-bot/src/modules/task/task.service.spec.ts b/alfy-bot/src/modules/task/task.service.spec.ts index 38538a4..4e3243e 100644 --- a/alfy-bot/src/modules/task/task.service.spec.ts +++ b/alfy-bot/src/modules/task/task.service.spec.ts @@ -530,6 +530,22 @@ describe('TaskService', () => { expect(result.pomodoroConfig).toBeNull(); }); + it('создаёт конфиг если его не было', async () => { + repo.findById.mockResolvedValue(makeTask({ pomodoroConfig: null })); + + const result = await service.updatePomodoroConfig(1, 'task-1', { + pomodoroCount: 2, + }); + + expect(repo.updatePomodoroConfig).toHaveBeenCalledWith( + expect.objectContaining({ id: 'task-1', pomodoroConfig: null }), + { pomodoroCount: 2 }, + ); + expect(result.pomodoroConfig).toEqual( + expect.objectContaining({ pomodoroCount: 2 }), + ); + }); + it('бросает NotFoundException для несуществующей задачи', async () => { repo.findById.mockResolvedValue(null);