You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
6.2 KiB
195 lines
6.2 KiB
5 months ago
|
/**
|
||
|
* タスク一覧を表示・管理するページコンポーネント
|
||
|
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供
|
||
|
*/
|
||
|
import React, { useState, useEffect } from 'react';
|
||
|
import { taskApi } from '../services/api';
|
||
|
import {
|
||
|
Container,
|
||
|
Typography,
|
||
|
List,
|
||
|
ListItem,
|
||
|
ListItemText,
|
||
|
ListItemSecondaryAction,
|
||
|
IconButton,
|
||
|
Checkbox,
|
||
|
Fab,
|
||
|
Dialog,
|
||
|
DialogTitle,
|
||
|
DialogContent,
|
||
|
DialogActions,
|
||
|
TextField,
|
||
|
Button,
|
||
|
Box,
|
||
|
} from '@mui/material';
|
||
|
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material';
|
||
|
import { Task } from '../types/types';
|
||
|
import { TASK_ERRORS } from '../constants/errorMessages';
|
||
|
|
||
|
// 新規タスクの初期状態
|
||
|
const EMPTY_TASK = { title: '', description: '', completed: false };
|
||
|
|
||
|
const TaskListPage: React.FC = () => {
|
||
|
// タスク一覧の状態管理
|
||
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||
|
// 新規タスク作成ダイアログの表示状態
|
||
|
const [openDialog, setOpenDialog] = useState(false);
|
||
|
// 新規タスクの入力内容
|
||
|
const [newTask, setNewTask] = useState(EMPTY_TASK);
|
||
|
|
||
|
// コンポーネントマウント時にタスク一覧を取得
|
||
|
useEffect(() => {
|
||
|
fetchTasks();
|
||
|
}, []);
|
||
|
|
||
|
/**
|
||
|
* APIからタスク一覧を取得する関数
|
||
|
* 取得したタスクをstate(tasks)に設定
|
||
|
*/
|
||
|
const fetchTasks = async () => {
|
||
|
try {
|
||
|
const tasks = await taskApi.getTasks();
|
||
|
setTasks(tasks);
|
||
|
} catch (error) {
|
||
|
console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* タスクの完了状態を切り替えるハンドラー
|
||
|
* 対象タスクの完了状態を反転させてAPIに更新を要求
|
||
|
*/
|
||
|
const handleToggleComplete = async (taskId: number) => {
|
||
|
try {
|
||
|
const task = tasks.find(t => t.id === taskId);
|
||
|
if (!task) return;
|
||
|
|
||
|
await taskApi.updateTask(taskId, { ...task, completed: !task.completed });
|
||
|
fetchTasks(); // 更新後のタスク一覧を再取得
|
||
|
} catch (error) {
|
||
|
console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* タスクを削除するハンドラー
|
||
|
* 指定されたIDのタスクをAPIを通じて削除
|
||
|
*/
|
||
|
const handleDeleteTask = async (taskId: number) => {
|
||
|
try {
|
||
|
await taskApi.deleteTask(taskId);
|
||
|
fetchTasks(); // 削除後のタスク一覧を再取得
|
||
|
} catch (error) {
|
||
|
console.error(`${TASK_ERRORS.DELETE_FAILED}:`, error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 新規タスクを作成するハンドラー
|
||
|
* 入力されたタスク情報をAPIに送信して新規作成
|
||
|
* 作成後はダイアログを閉じ、入力内容をリセット
|
||
|
*/
|
||
|
const handleCreateTask = async () => {
|
||
|
try {
|
||
|
await taskApi.createTask(newTask);
|
||
|
setOpenDialog(false); // ダイアログを閉じる
|
||
|
setNewTask(EMPTY_TASK); // 入力内容をリセット
|
||
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
||
|
} catch (error) {
|
||
|
console.error(`${TASK_ERRORS.CREATE_FAILED}:`, error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<Container>
|
||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||
|
タスク一覧
|
||
|
</Typography>
|
||
|
{/* タスク一覧表示エリア - 青い背景のコンテナ */}
|
||
|
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px'}}>
|
||
|
<List>
|
||
|
{/* タスク一覧をマップして各タスクをリストアイテムとして表示 */}
|
||
|
{tasks.map((task) => (
|
||
|
<ListItem
|
||
|
key={task.id}
|
||
|
sx={{
|
||
|
bgcolor: 'background.paper',
|
||
|
mb: 1,
|
||
|
borderRadius: 1,
|
||
|
boxShadow: 1,
|
||
|
}}
|
||
|
>
|
||
|
{/* タスク完了状態を切り替えるチェックボックス */}
|
||
|
<Checkbox
|
||
|
checked={task.completed}
|
||
|
onChange={() => handleToggleComplete(task.id)}
|
||
|
/>
|
||
|
{/* タスクのタイトルと説明 - 完了状態に応じて取り消し線を表示 */}
|
||
|
<ListItemText
|
||
|
primary={task.title}
|
||
|
secondary={task.description}
|
||
|
sx={{
|
||
|
textDecoration: task.completed ? 'line-through' : 'none',
|
||
|
}}
|
||
|
/>
|
||
|
{/* タスク削除ボタン */}
|
||
|
<ListItemSecondaryAction>
|
||
|
<IconButton
|
||
|
edge="end"
|
||
|
aria-label="delete"
|
||
|
onClick={() => handleDeleteTask(task.id)}
|
||
|
>
|
||
|
<DeleteIcon />
|
||
|
</IconButton>
|
||
|
</ListItemSecondaryAction>
|
||
|
</ListItem>
|
||
|
))}
|
||
|
</List>
|
||
|
</div>
|
||
|
{/* 新規タスク作成ボタン - 画面下部に固定表示 */}
|
||
|
<Fab
|
||
|
color="primary"
|
||
|
sx={{ position: 'fixed', bottom: 16, left: '50%', transform: 'translateX(-50%)'}}
|
||
|
onClick={() => setOpenDialog(true)}
|
||
|
>
|
||
|
<AddIcon />
|
||
|
</Fab>
|
||
|
|
||
|
{/* 新規タスク作成ダイアログ */}
|
||
|
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true}>
|
||
|
<DialogTitle>新規タスク</DialogTitle>
|
||
|
<DialogContent>
|
||
|
<Box sx={{ pt: 1 }}>
|
||
|
{/* タスクタイトル入力フィールド */}
|
||
|
<TextField
|
||
|
autoFocus
|
||
|
margin="dense"
|
||
|
label="タイトル"
|
||
|
fullWidth
|
||
|
value={newTask.title}
|
||
|
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
|
||
|
/>
|
||
|
{/* タスク説明入力フィールド - 複数行入力可能 */}
|
||
|
<TextField
|
||
|
margin="dense"
|
||
|
label="説明"
|
||
|
fullWidth
|
||
|
multiline
|
||
|
rows={4}
|
||
|
value={newTask.description}
|
||
|
onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
|
||
|
/>
|
||
|
</Box>
|
||
|
</DialogContent>
|
||
|
<DialogActions>
|
||
|
<Button onClick={() => setOpenDialog(false)}>キャンセル</Button>
|
||
|
<Button onClick={handleCreateTask} variant="contained">
|
||
|
作成
|
||
|
</Button>
|
||
|
</DialogActions>
|
||
|
</Dialog>
|
||
|
</Container>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default TaskListPage;
|