Skip to content

Commit 33d3c3d

Browse files
authored
Merge pull request #67 from nannany/copilot/add-interrupt-button-feature
feat: タスク中断ボタンの実装
2 parents 307df20 + 2d2de32 commit 33d3c3d

File tree

8 files changed

+270
-4
lines changed

8 files changed

+270
-4
lines changed

ui/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
node_modules/
1+
node_modules/
2+
dist/

ui/.prettierignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Build artifacts
2+
dist/
3+
build/
4+
5+
# Dependencies
6+
node_modules/
7+
8+
# Coverage
9+
coverage/
10+
11+
# Generated files
12+
*.log

ui/src/components/CurrentTaskFooter.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { useEffect, useState } from "react";
22
import { Task, Category } from "@/types/task";
3-
import { Square } from "lucide-react";
3+
import { Square, Pause } from "lucide-react";
44
import { Button } from "@/components/ui/button";
55
import { formatTimeAsHHmm } from "@/lib/utils";
66

77
interface CurrentTaskFooterProps {
88
currentTask: Task | null;
99
categories: Category[];
1010
onTaskTimer: (taskId: string, action: "start" | "stop" | "complete") => void;
11+
onPauseTask: (task: Task) => void;
1112
}
1213

1314
export const CurrentTaskFooter = ({
1415
currentTask,
1516
categories,
1617
onTaskTimer,
18+
onPauseTask,
1719
}: CurrentTaskFooterProps) => {
1820
const [currentTime, setCurrentTime] = useState(new Date());
1921
const [elapsedTime, setElapsedTime] = useState(0);
@@ -93,6 +95,20 @@ export const CurrentTaskFooter = ({
9395
<p className="text-xs text-gray-500">経過時間</p>
9496
</div>
9597

98+
<Button
99+
size="sm"
100+
variant="outline"
101+
onClick={() => onPauseTask(currentTask)}
102+
className="hover:bg-orange-50"
103+
style={{
104+
color: "#ea580c",
105+
borderColor: "#ea580c",
106+
}}
107+
>
108+
<Pause className="h-4 w-4 mr-1" />
109+
中断
110+
</Button>
111+
96112
<Button
97113
size="sm"
98114
variant="outline"

ui/src/components/task/SortableTask.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useSortable } from "@dnd-kit/sortable";
22
import { CSS } from "@dnd-kit/utilities";
33
import { Button } from "@/components/ui/button";
4-
import { GripVertical, Trash2, CalendarCheck } from "lucide-react";
4+
import { GripVertical, Trash2, CalendarCheck, Pause } from "lucide-react";
55
import React from "react";
66
import { TaskTimerButton } from "./TaskTimerButton";
77
import { TaskTitleField } from "./TaskTitleField";
@@ -106,6 +106,9 @@ const SortableTask = ({ task }: SortableTaskProps) => {
106106
// タスクが完了しているかどうか
107107
const isCompleted = !!task.end_time;
108108

109+
// タスクが実行中かどうか
110+
const isRunning = !!task.start_time && !task.end_time;
111+
109112
return (
110113
<div
111114
ref={setNodeRef}
@@ -154,6 +157,18 @@ const SortableTask = ({ task }: SortableTaskProps) => {
154157
</div>
155158

156159
<div className="flex gap-2 items-center">
160+
{/* 中断ボタン:実行中のタスクのみ表示 */}
161+
{isRunning && (
162+
<Button
163+
size="icon"
164+
variant="outline"
165+
onClick={() => taskActions.handlePauseTask(task)}
166+
className="h-8 w-8 text-orange-500 hover:text-orange-700 hover:bg-orange-50"
167+
title="中断"
168+
>
169+
<Pause className="h-4 w-4" />
170+
</Button>
171+
)}
157172
{/* 今日に移動ボタン:完了していない&今日以外のタスクに表示 */}
158173
{!isCompleted && !isToday && (
159174
<Button

ui/src/contexts/TaskContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface TaskActions {
3232
) => void;
3333
handleRepeatTask: (task: Task) => void;
3434
handleMoveToToday: (taskId: string) => void;
35+
handlePauseTask: (task: Task) => void;
3536
}
3637

3738
// タスクコンテキストの型定義
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, it, expect } from "vitest";
2+
import { taskReducer } from "@/reducers/taskReducer";
3+
import { Task } from "@/types/task";
4+
5+
describe("Task Pause Functionality", () => {
6+
const mockRunningTask: Task = {
7+
id: "1",
8+
user_id: "user1",
9+
title: "タスク1",
10+
description: "説明",
11+
estimated_minute: 30,
12+
task_order: 1,
13+
start_time: "2024-01-01T10:00:00Z",
14+
end_time: null,
15+
category_id: "cat1",
16+
created_at: "2024-01-01T00:00:00Z",
17+
task_date: "2024-01-01",
18+
};
19+
20+
const mockTask2: Task = {
21+
id: "2",
22+
user_id: "user1",
23+
title: "タスク2",
24+
description: "",
25+
estimated_minute: 20,
26+
task_order: 2,
27+
start_time: null,
28+
end_time: null,
29+
category_id: null,
30+
created_at: "2024-01-01T00:00:00Z",
31+
task_date: "2024-01-01",
32+
};
33+
34+
describe("pause task behavior (completing task and creating new one)", () => {
35+
it("should complete a running task by setting end_time", () => {
36+
const initialState = [mockRunningTask, mockTask2];
37+
38+
// タスクを完了させる(end_timeを設定)
39+
const action = {
40+
type: "UPDATE_TASK" as const,
41+
payload: {
42+
id: "1",
43+
end_time: "2024-01-01T11:00:00Z",
44+
},
45+
};
46+
47+
const newState = taskReducer(initialState, action);
48+
49+
// タスクが完了していることを確認
50+
const completedTask = newState.find((t) => t.id === "1");
51+
expect(completedTask?.end_time).toBe("2024-01-01T11:00:00Z");
52+
});
53+
54+
it("should add a new task with same attributes after pausing", () => {
55+
const initialState = [mockTask2];
56+
57+
// 同じ属性で新しいタスクを追加
58+
const newTask: Task = {
59+
id: "3",
60+
user_id: mockRunningTask.user_id,
61+
title: mockRunningTask.title,
62+
description: mockRunningTask.description,
63+
estimated_minute: mockRunningTask.estimated_minute,
64+
task_order: null,
65+
start_time: null,
66+
end_time: null,
67+
category_id: mockRunningTask.category_id,
68+
created_at: "2024-01-01T11:00:00Z",
69+
task_date: mockRunningTask.task_date,
70+
};
71+
72+
const action = {
73+
type: "ADD_TASK" as const,
74+
payload: newTask,
75+
};
76+
77+
const newState = taskReducer(initialState, action);
78+
79+
// 新しいタスクが追加されたことを確認
80+
expect(newState).toHaveLength(2);
81+
const addedTask = newState.find((t) => t.id === "3");
82+
expect(addedTask).toBeDefined();
83+
expect(addedTask?.title).toBe(mockRunningTask.title);
84+
expect(addedTask?.estimated_minute).toBe(
85+
mockRunningTask.estimated_minute,
86+
);
87+
expect(addedTask?.category_id).toBe(mockRunningTask.category_id);
88+
expect(addedTask?.start_time).toBeNull();
89+
expect(addedTask?.end_time).toBeNull();
90+
});
91+
92+
it("should complete original task and add new task in sequence (simulating pause)", () => {
93+
const initialState = [mockRunningTask, mockTask2];
94+
95+
// ステップ1: 実行中のタスクを完了
96+
const completeAction = {
97+
type: "UPDATE_TASK" as const,
98+
payload: {
99+
id: "1",
100+
end_time: "2024-01-01T11:00:00Z",
101+
},
102+
};
103+
104+
const stateAfterComplete = taskReducer(initialState, completeAction);
105+
const completedTask = stateAfterComplete.find((t) => t.id === "1");
106+
expect(completedTask?.end_time).toBe("2024-01-01T11:00:00Z");
107+
108+
// ステップ2: 同じ属性で新しいタスクを追加
109+
const newTask: Task = {
110+
id: "3",
111+
user_id: mockRunningTask.user_id,
112+
title: mockRunningTask.title,
113+
description: mockRunningTask.description,
114+
estimated_minute: mockRunningTask.estimated_minute,
115+
task_order: null,
116+
start_time: null,
117+
end_time: null,
118+
category_id: mockRunningTask.category_id,
119+
created_at: "2024-01-01T11:00:00Z",
120+
task_date: mockRunningTask.task_date,
121+
};
122+
123+
const addAction = {
124+
type: "ADD_TASK" as const,
125+
payload: newTask,
126+
};
127+
128+
const finalState = taskReducer(stateAfterComplete, addAction);
129+
130+
// 最終的な状態を確認
131+
expect(finalState).toHaveLength(3);
132+
133+
// 元のタスクが完了している
134+
const original = finalState.find((t) => t.id === "1");
135+
expect(original?.end_time).toBe("2024-01-01T11:00:00Z");
136+
137+
// 新しいタスクが未開始状態で追加されている
138+
const newTaskInState = finalState.find((t) => t.id === "3");
139+
expect(newTaskInState).toBeDefined();
140+
expect(newTaskInState?.title).toBe(mockRunningTask.title);
141+
expect(newTaskInState?.start_time).toBeNull();
142+
expect(newTaskInState?.end_time).toBeNull();
143+
});
144+
});
145+
});

ui/src/hooks/useTaskActions.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { createClient } from "@/lib/supabase/client";
33
import { useToast } from "@/components/ui/use-toast";
4-
import { TaskAction } from "@/types/task";
4+
import { TaskAction, Task } from "@/types/task";
55
import { getTodayDateString } from "@/lib/utils";
66

77
const supabase = createClient();
@@ -85,9 +85,84 @@ export const useTaskActions = (dispatch: React.Dispatch<TaskAction>) => {
8585
}
8686
};
8787

88+
// タスクを中断
89+
const handlePauseTask = async (task: Task) => {
90+
// 1. 現在のタスクに終了時刻を設定して完了させる
91+
const endTime = new Date().toISOString();
92+
const { error: updateError } = await supabase
93+
.from("tasks")
94+
.update({ end_time: endTime })
95+
.eq("id", task.id);
96+
97+
if (updateError) {
98+
toast({
99+
title: "Error",
100+
description: "タスクの中断に失敗しました",
101+
variant: "destructive",
102+
});
103+
console.error("Error pausing task:", updateError);
104+
return;
105+
}
106+
107+
// ローカル状態を更新
108+
dispatch({
109+
type: "UPDATE_TASK",
110+
payload: { id: task.id, end_time: endTime },
111+
});
112+
113+
// 2. 同じ属性で新しいタスクを作成
114+
const newTask = {
115+
title: task.title,
116+
description: task.description,
117+
user_id: task.user_id,
118+
estimated_minute: task.estimated_minute,
119+
category_id: task.category_id,
120+
task_date: task.task_date,
121+
task_order: null,
122+
};
123+
124+
const { data, error: insertError } = await supabase
125+
.from("tasks")
126+
.insert(newTask)
127+
.select();
128+
129+
if (insertError) {
130+
toast({
131+
title: "Error",
132+
description: "新しいタスクの作成に失敗しました",
133+
variant: "destructive",
134+
});
135+
console.error("Error creating new task:", insertError);
136+
return;
137+
}
138+
139+
if (!data || data.length === 0) {
140+
toast({
141+
title: "Error",
142+
description: "新しいタスクの作成に失敗しました",
143+
variant: "destructive",
144+
});
145+
console.error("Error creating new task: No data returned");
146+
return;
147+
}
148+
149+
// 新しく作成したタスクをリストに追加
150+
const createdTask = data[0] as unknown as Task;
151+
dispatch({
152+
type: "ADD_TASK",
153+
payload: createdTask,
154+
});
155+
156+
toast({
157+
title: "Success",
158+
description: `タスク "${task.title}" を中断しました`,
159+
});
160+
};
161+
88162
return {
89163
handleDelete,
90164
handleTaskTimer,
91165
handleMoveToToday,
166+
handlePauseTask,
92167
};
93168
};

ui/src/pages/TaskList/TaskList.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ const TaskList = () => {
683683
currentTask={currentRunningTask || null}
684684
categories={categories}
685685
onTaskTimer={taskActions.handleTaskTimer}
686+
onPauseTask={taskActions.handlePauseTask}
686687
/>
687688
</TaskProvider>
688689
);

0 commit comments

Comments
 (0)