|
|
|
@ -3,10 +3,11 @@ |
|
|
|
|
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供 |
|
|
|
|
*/ |
|
|
|
|
import React, { useState, useEffect } from 'react'; |
|
|
|
|
import { taskApi } from '../services/api'; |
|
|
|
|
import { toBuyApi, stuffApi } from '../services/api'; |
|
|
|
|
import { |
|
|
|
|
Container, |
|
|
|
|
Typography, |
|
|
|
|
Tooltip, |
|
|
|
|
List, |
|
|
|
|
ListItem, |
|
|
|
|
ListItemText, |
|
|
|
@ -21,21 +22,51 @@ import { |
|
|
|
|
TextField, |
|
|
|
|
Button, |
|
|
|
|
Box, |
|
|
|
|
FormControlLabel, |
|
|
|
|
FormGroup, |
|
|
|
|
FormControl, |
|
|
|
|
InputLabel, |
|
|
|
|
Select, |
|
|
|
|
MenuItem |
|
|
|
|
} from '@mui/material'; |
|
|
|
|
import { Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'; |
|
|
|
|
import { Task } from '../types/types'; |
|
|
|
|
import { |
|
|
|
|
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, |
|
|
|
|
SoupKitchen as SoupKitchenIcon |
|
|
|
|
} from '@mui/icons-material'; |
|
|
|
|
import { Task, ToBuy, Stuff } from '../types/types'; |
|
|
|
|
import { TASK_ERRORS } from '../constants/errorMessages'; |
|
|
|
|
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
|
|
|
|
|
import CategoryDropDown from "../components/CategoryDropDown"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 新規タスクの初期状態
|
|
|
|
|
const EMPTY_TASK = { title: '', description: '', completed: false }; |
|
|
|
|
const EMPTY_TASK: Omit<ToBuy, 'tobuy_id' | 'stuff_id'> & { stuff_id: number | null, category: string } & { newAddition: boolean } = { |
|
|
|
|
stuff_id: null, |
|
|
|
|
stuff_name: '', |
|
|
|
|
amount: 0, |
|
|
|
|
shop: '', |
|
|
|
|
category: '', |
|
|
|
|
newAddition: false, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const TaskListPage: React.FC = () => { |
|
|
|
|
// タスク一覧の状態管理
|
|
|
|
|
const [tasks, setTasks] = useState<Task[]>([]); |
|
|
|
|
const [tobuys, setToBuys] = useState<ToBuy[]>([]); |
|
|
|
|
// 新規タスク作成ダイアログの表示状態
|
|
|
|
|
const [openDialog, setOpenDialog] = useState(false); |
|
|
|
|
// 新規タスクの入力内容
|
|
|
|
|
const [newTask, setNewTask] = useState(EMPTY_TASK); |
|
|
|
|
|
|
|
|
|
//在庫登録ダイアログの表示状態
|
|
|
|
|
const [openInfoDialog, setOpenInfoDialog] = useState(false); |
|
|
|
|
|
|
|
|
|
const [selectedTask, setSelectedTask] = useState<ToBuy>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [newToBuy, setNewToBuy] = useState(EMPTY_TASK); |
|
|
|
|
|
|
|
|
|
const [stuffs, setStuffs] = useState<Stuff[]>([]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// コンポーネントマウント時にタスク一覧を取得
|
|
|
|
|
useEffect(() => { |
|
|
|
@ -48,36 +79,42 @@ const TaskListPage: React.FC = () => { |
|
|
|
|
*/ |
|
|
|
|
const fetchTasks = async () => { |
|
|
|
|
try { |
|
|
|
|
const tasks = await taskApi.getTasks(); |
|
|
|
|
setTasks(tasks); |
|
|
|
|
const tobuys = await toBuyApi.getToBuys(); |
|
|
|
|
setToBuys(tobuys.tobuy_array); |
|
|
|
|
} 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; |
|
|
|
|
const onChangeCategory = async (category: string) => { |
|
|
|
|
setNewToBuy({ ...newToBuy, category }) |
|
|
|
|
const result = await stuffApi.getStuffs(category) |
|
|
|
|
setStuffs(result.stuff_array) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await taskApi.updateTask(taskId, { ...task, completed: !task.completed }); |
|
|
|
|
fetchTasks(); // 更新後のタスク一覧を再取得
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
// /**
|
|
|
|
|
// * タスクの完了状態を切り替えるハンドラー
|
|
|
|
|
// * 対象タスクの完了状態を反転させてAPIに更新を要求
|
|
|
|
|
// */
|
|
|
|
|
// const handleToggleComplete = async (taskId: number) => {
|
|
|
|
|
// try {
|
|
|
|
|
// const task = tasks.find(t => t.id === taskId);
|
|
|
|
|
// if (!task) return;
|
|
|
|
|
|
|
|
|
|
// await toBuyApi.updateTask(taskId, { ...task, completed: !task.completed });
|
|
|
|
|
// fetchTasks(); // 更新後のタスク一覧を再取得
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// console.error(`${TASK_ERRORS.UPDATE_FAILED}:`, error);
|
|
|
|
|
// }
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* タスクを削除するハンドラー |
|
|
|
|
* 指定されたIDのタスクをAPIを通じて削除 |
|
|
|
|
*/ |
|
|
|
|
const handleDeleteTask = async (taskId: number) => { |
|
|
|
|
const handleDeleteTask = async (toBuyId: number) => { |
|
|
|
|
try { |
|
|
|
|
await taskApi.deleteTask(taskId); |
|
|
|
|
await toBuyApi.deleteToBuy(toBuyId); |
|
|
|
|
fetchTasks(); // 削除後のタスク一覧を再取得
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.error(`${TASK_ERRORS.DELETE_FAILED}:`, error); |
|
|
|
@ -91,9 +128,9 @@ const TaskListPage: React.FC = () => { |
|
|
|
|
*/ |
|
|
|
|
const handleCreateTask = async () => { |
|
|
|
|
try { |
|
|
|
|
await taskApi.createTask(newTask); |
|
|
|
|
await toBuyApi.addToBuy(newToBuy); |
|
|
|
|
setOpenDialog(false); // ダイアログを閉じる
|
|
|
|
|
setNewTask(EMPTY_TASK); // 入力内容をリセット
|
|
|
|
|
setNewToBuy(EMPTY_TASK); // 入力内容をリセット
|
|
|
|
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
|
|
|
|
} catch (error) { |
|
|
|
|
console.error(`${TASK_ERRORS.CREATE_FAILED}:`, error); |
|
|
|
@ -106,12 +143,12 @@ const TaskListPage: React.FC = () => { |
|
|
|
|
タスク一覧 |
|
|
|
|
</Typography> |
|
|
|
|
{/* タスク一覧表示エリア - 青い背景のコンテナ */} |
|
|
|
|
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px'}}> |
|
|
|
|
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px' }}> |
|
|
|
|
<List> |
|
|
|
|
{/* タスク一覧をマップして各タスクをリストアイテムとして表示 */} |
|
|
|
|
{tasks.map((task) => ( |
|
|
|
|
{tobuys.map((tobuy) => ( |
|
|
|
|
<ListItem |
|
|
|
|
key={task.id} |
|
|
|
|
key={tobuy.tobuy_id} |
|
|
|
|
sx={{ |
|
|
|
|
bgcolor: 'background.paper', |
|
|
|
|
mb: 1, |
|
|
|
@ -120,75 +157,210 @@ const TaskListPage: React.FC = () => { |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
{/* タスク完了状態を切り替えるチェックボックス */} |
|
|
|
|
{/*} |
|
|
|
|
<Checkbox |
|
|
|
|
checked={task.completed} |
|
|
|
|
onChange={() => handleToggleComplete(task.id)} |
|
|
|
|
/> |
|
|
|
|
*/} |
|
|
|
|
{/* タスクのタイトルと説明 - 完了状態に応じて取り消し線を表示 */} |
|
|
|
|
<ListItemText |
|
|
|
|
primary={task.title} |
|
|
|
|
secondary={task.description} |
|
|
|
|
primary={tobuy.stuff_name} |
|
|
|
|
secondary={tobuy.amount} |
|
|
|
|
sx={{ |
|
|
|
|
textDecoration: task.completed ? 'line-through' : 'none', |
|
|
|
|
textDecoration: false ? 'line-through' : 'none', |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
{/* タスク削除ボタン */} |
|
|
|
|
{/* 買い物リスト:食材情報記入ボタン */} |
|
|
|
|
<ListItemSecondaryAction> |
|
|
|
|
<IconButton |
|
|
|
|
edge="end" |
|
|
|
|
aria-label="delete" |
|
|
|
|
onClick={() => handleDeleteTask(task.id)} |
|
|
|
|
<Tooltip title="食材情報追加"> |
|
|
|
|
<IconButton |
|
|
|
|
edge="end" |
|
|
|
|
aria-label="食材情報追加" |
|
|
|
|
onClick={() => setOpenInfoDialog(true)} |
|
|
|
|
//onClick={() => handleDeleteTask(task.id)}
|
|
|
|
|
> |
|
|
|
|
<ShoppingBasketIcon /> |
|
|
|
|
</IconButton> |
|
|
|
|
</Tooltip> |
|
|
|
|
{/* 買い物リスト:食材削除ボタン */} |
|
|
|
|
<Tooltip title="項目を削除" |
|
|
|
|
componentsProps={{ |
|
|
|
|
tooltip: { |
|
|
|
|
sx: { |
|
|
|
|
backgroundColor: "white", |
|
|
|
|
color: "red", |
|
|
|
|
fontSize: "0.8rem", |
|
|
|
|
padding: "6px", |
|
|
|
|
borderRadius: "6px", |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<DeleteIcon /> |
|
|
|
|
</IconButton> |
|
|
|
|
|
|
|
|
|
<IconButton |
|
|
|
|
edge="end" |
|
|
|
|
aria-label="delete" |
|
|
|
|
onClick={() => handleDeleteTask(tobuy.tobuy_id)} |
|
|
|
|
> |
|
|
|
|
<DeleteIcon /> |
|
|
|
|
</IconButton> |
|
|
|
|
</Tooltip> |
|
|
|
|
</ListItemSecondaryAction> |
|
|
|
|
|
|
|
|
|
</ListItem> |
|
|
|
|
))} |
|
|
|
|
</List> |
|
|
|
|
</div> |
|
|
|
|
{/* 新規タスク作成ボタン - 画面下部に固定表示 */} |
|
|
|
|
<Fab |
|
|
|
|
color="primary" |
|
|
|
|
sx={{ position: 'fixed', bottom: 16, left: '50%', transform: 'translateX(-50%)'}} |
|
|
|
|
onClick={() => setOpenDialog(true)} |
|
|
|
|
> |
|
|
|
|
<AddIcon /> |
|
|
|
|
</Fab> |
|
|
|
|
{/* 新規材料作成ボタン - 画面下部に固定表示 */} |
|
|
|
|
<Tooltip title="材料のみ追加"> |
|
|
|
|
<Fab |
|
|
|
|
color="primary" |
|
|
|
|
sx={{ position: 'fixed', bottom: 16, left: '40%', transform: 'translateX(-50%)' }} |
|
|
|
|
onClick={() => setOpenDialog(true)} |
|
|
|
|
> |
|
|
|
|
<AddIcon /> |
|
|
|
|
</Fab> |
|
|
|
|
</Tooltip> |
|
|
|
|
{/*新規料理追加ボタン - 画面下部に固定表示 */} |
|
|
|
|
<Tooltip title="料理から追加"> |
|
|
|
|
<Fab |
|
|
|
|
color="primary" |
|
|
|
|
sx={{ position: 'fixed', bottom: 16, left: '60%', transform: 'translateX(-50%)' }} |
|
|
|
|
onClick={() => setOpenDialog(true)} |
|
|
|
|
> |
|
|
|
|
<SoupKitchenIcon /> |
|
|
|
|
</Fab> |
|
|
|
|
</Tooltip> |
|
|
|
|
|
|
|
|
|
{/* 新規タスク作成ダイアログ */} |
|
|
|
|
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true}> |
|
|
|
|
<DialogTitle>新規タスク</DialogTitle> |
|
|
|
|
<Box display="flex" alignItems="center"> |
|
|
|
|
<DialogTitle sx={{ flexGrow: 1 }}>材料の追加</DialogTitle> |
|
|
|
|
<FormGroup row> |
|
|
|
|
<FormControlLabel |
|
|
|
|
control={<Checkbox />} |
|
|
|
|
label="食材を新規追加" |
|
|
|
|
checked={newToBuy.newAddition} |
|
|
|
|
onChange={(e) => setNewToBuy({ ...newToBuy, newAddition: (e.target as HTMLInputElement).checked })} |
|
|
|
|
/> |
|
|
|
|
</FormGroup> |
|
|
|
|
</Box> |
|
|
|
|
<DialogContent> |
|
|
|
|
<Box sx={{ pt: 1 }}> |
|
|
|
|
{/*材料カテゴリ選択 */} |
|
|
|
|
|
|
|
|
|
<FormControl sx={{ width: "50%", marginBottom: 2 }}> |
|
|
|
|
<InputLabel id="demo-simple-select-label">カテゴリ</InputLabel> |
|
|
|
|
<Select |
|
|
|
|
labelId="demo-simple-select-label" |
|
|
|
|
value={newToBuy.category} |
|
|
|
|
onChange={(e) => onChangeCategory(e.target.value) } |
|
|
|
|
> |
|
|
|
|
<MenuItem value="乳製品">乳製品</MenuItem> |
|
|
|
|
<MenuItem value="肉・魚">肉・魚</MenuItem> |
|
|
|
|
<MenuItem value="野菜">野菜</MenuItem> |
|
|
|
|
<MenuItem value="調味料">調味料</MenuItem> |
|
|
|
|
<MenuItem value="その他">その他</MenuItem> |
|
|
|
|
</Select> |
|
|
|
|
</FormControl> |
|
|
|
|
|
|
|
|
|
{!newToBuy.newAddition && <FormControl sx={{ width: "50%", marginBottom: 2 }}> |
|
|
|
|
<InputLabel id="demo-simple-select-label">材料名(選択)</InputLabel> |
|
|
|
|
<Select |
|
|
|
|
labelId="demo-simple-select-label" |
|
|
|
|
value={newToBuy.stuff_id} |
|
|
|
|
onChange={(e) => setNewToBuy({ ...newToBuy, stuff_id: Number(e.target.value) })} |
|
|
|
|
> |
|
|
|
|
{stuffs.map((stuff) => ( |
|
|
|
|
<MenuItem key={stuff.stuff_id} value={stuff.stuff_id}> |
|
|
|
|
{stuff.stuff_name} |
|
|
|
|
</MenuItem> |
|
|
|
|
))} |
|
|
|
|
</Select> |
|
|
|
|
</FormControl>} |
|
|
|
|
|
|
|
|
|
{/* タスクタイトル入力フィールド */} |
|
|
|
|
{newToBuy.newAddition && <TextField |
|
|
|
|
autoFocus |
|
|
|
|
margin="dense" |
|
|
|
|
label="材料名" |
|
|
|
|
fullWidth |
|
|
|
|
value={newToBuy.stuff_name} |
|
|
|
|
onChange={(e) => setNewToBuy({ ...newToBuy, stuff_name: e.target.value })} |
|
|
|
|
sx={{ marginBottom: 2 }} |
|
|
|
|
/>} |
|
|
|
|
{/* 数量入力フィールド */} |
|
|
|
|
<TextField |
|
|
|
|
margin="dense" |
|
|
|
|
label="数量" |
|
|
|
|
fullWidth |
|
|
|
|
value={newToBuy.amount} |
|
|
|
|
onChange={(e) => { |
|
|
|
|
const value = e.target.value; |
|
|
|
|
const parsedValue = parseInt(value, 10); // 数値に変換
|
|
|
|
|
if (!isNaN(parsedValue)) { |
|
|
|
|
setNewToBuy({ ...newToBuy, amount: parsedValue }); // number型で保存
|
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
sx={{ width: "20%" }} |
|
|
|
|
type="number" |
|
|
|
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
|
|
|
|
|
|
|
|
|
|
/> |
|
|
|
|
</Box> |
|
|
|
|
</DialogContent> |
|
|
|
|
<DialogActions> |
|
|
|
|
<Button onClick={() => setOpenDialog(false)}>キャンセル</Button> |
|
|
|
|
<Button onClick={handleCreateTask} variant="contained"> |
|
|
|
|
追加 |
|
|
|
|
</Button> |
|
|
|
|
</DialogActions> |
|
|
|
|
</Dialog> |
|
|
|
|
|
|
|
|
|
{/*在庫登録のための数値入力ダイアログ */} |
|
|
|
|
<Dialog open={openInfoDialog} onClose={() => setOpenInfoDialog(false)} disableScrollLock={true}> |
|
|
|
|
<DialogTitle>在庫登録</DialogTitle> |
|
|
|
|
<DialogContent> |
|
|
|
|
<Box sx={{ pt: 1 }}> |
|
|
|
|
{/* 価格入力フィールド */} |
|
|
|
|
<TextField |
|
|
|
|
autoFocus |
|
|
|
|
margin="dense" |
|
|
|
|
label="タイトル" |
|
|
|
|
label="価格" |
|
|
|
|
fullWidth |
|
|
|
|
/> |
|
|
|
|
{/* 消費・賞味期限入力フィールド */} |
|
|
|
|
<TextField |
|
|
|
|
margin="dense" |
|
|
|
|
label="消費・賞味期限(yyyy/MM/dd)" |
|
|
|
|
fullWidth |
|
|
|
|
value={newTask.title} |
|
|
|
|
onChange={(e) => setNewTask({ ...newTask, title: e.target.value })} |
|
|
|
|
multiline |
|
|
|
|
/> |
|
|
|
|
{/* タスク説明入力フィールド - 複数行入力可能 */} |
|
|
|
|
{/* 購入日入力フィールド */} |
|
|
|
|
<TextField |
|
|
|
|
margin="dense" |
|
|
|
|
label="説明" |
|
|
|
|
label="購入日(yyyy/MM/dd)" |
|
|
|
|
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 onClick={() => setOpenInfoDialog(false)}>キャンセル</Button> |
|
|
|
|
<Button onClick={() => { |
|
|
|
|
if (selectedTask) { |
|
|
|
|
handleDeleteTask(selectedTask.tobuy_id) |
|
|
|
|
setOpenInfoDialog(false) |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
variant="contained"> |
|
|
|
|
登録 |
|
|
|
|
</Button> |
|
|
|
|
</DialogActions> |
|
|
|
|
</Dialog> |
|
|
|
|
</Container> |
|
|
|
|
|
|
|
|
|
); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|