|
|
/**
|
|
|
* タスク一覧を表示・管理するページコンポーネント
|
|
|
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供
|
|
|
*/
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
import { toBuyApi, stuffApi } from '../services/api';
|
|
|
import {
|
|
|
Container,
|
|
|
Typography,
|
|
|
Tooltip,
|
|
|
List,
|
|
|
ListItem,
|
|
|
ListItemText,
|
|
|
ListItemSecondaryAction,
|
|
|
IconButton,
|
|
|
Checkbox,
|
|
|
Fab,
|
|
|
Dialog,
|
|
|
DialogTitle,
|
|
|
DialogContent,
|
|
|
DialogActions,
|
|
|
TextField,
|
|
|
Button,
|
|
|
Box,
|
|
|
FormControlLabel,
|
|
|
FormGroup,
|
|
|
FormControl,
|
|
|
InputLabel,
|
|
|
Select,
|
|
|
MenuItem
|
|
|
} from '@mui/material';
|
|
|
import {
|
|
|
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
|
|
|
SoupKitchen as SoupKitchenIcon
|
|
|
} from '@mui/icons-material';
|
|
|
import { ToBuy, Stuff, Stock } from '../types/types';
|
|
|
import { TOBUY_ERRORS } from '../constants/errorMessages';
|
|
|
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
|
|
|
|
|
|
|
|
|
|
|
|
// 新規タスクの初期状態
|
|
|
const EMPTY_TOBUY: Omit<ToBuy, 'tobuyId' | 'stuffId'> & { stuffId: number | null, category: string } & { newAddition: boolean } = {
|
|
|
stuffId: null,
|
|
|
stuffName: '',
|
|
|
amount: 0,
|
|
|
shop: '',
|
|
|
category: '',
|
|
|
newAddition: false,
|
|
|
}
|
|
|
|
|
|
const EMPTY_STOCK = {
|
|
|
price: 0,
|
|
|
buyDate: '',
|
|
|
expDate: '',
|
|
|
}
|
|
|
|
|
|
const TaskListPage: React.FC = () => {
|
|
|
// タスク一覧の状態管理
|
|
|
const [tobuys, setToBuys] = useState<ToBuy[]>([]);
|
|
|
// 新規タスク作成ダイアログの表示状態
|
|
|
const [openDialog, setOpenDialog] = useState(false);
|
|
|
|
|
|
//在庫登録ダイアログの表示状態
|
|
|
const [openInfoDialog, setOpenInfoDialog] = useState(false);
|
|
|
|
|
|
const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0);
|
|
|
|
|
|
const [newToBuy, setNewToBuy] = useState(EMPTY_TOBUY);
|
|
|
|
|
|
const [stuffs, setStuffs] = useState<Stuff[]>([]);
|
|
|
|
|
|
const [newStock, setNewStock] = useState(EMPTY_STOCK);
|
|
|
|
|
|
|
|
|
// コンポーネントマウント時にタスク一覧を取得
|
|
|
useEffect(() => {
|
|
|
fetchTasks();
|
|
|
}, []);
|
|
|
|
|
|
/**
|
|
|
* APIからタスク一覧を取得する関数
|
|
|
* 取得したタスクをstate(tasks)に設定
|
|
|
*/
|
|
|
const fetchTasks = async () => {
|
|
|
try {
|
|
|
const tobuys = await toBuyApi.getToBuys();
|
|
|
setToBuys(tobuys);
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.FETCH_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const onChangeCategory = async (category: string) => {
|
|
|
setNewToBuy({ ...newToBuy, category })
|
|
|
const result = await stuffApi.getStuffs(category)
|
|
|
setStuffs(result)
|
|
|
}
|
|
|
|
|
|
// /**
|
|
|
// * タスクの完了状態を切り替えるハンドラー
|
|
|
// * 対象タスクの完了状態を反転させて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 (toBuyId: number) => {
|
|
|
try {
|
|
|
await toBuyApi.deleteToBuy(toBuyId);
|
|
|
fetchTasks(); // 削除後の買うもの一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.DELETE_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 買うものリストの在庫登録(購入処理)を行うハンドラー
|
|
|
*/
|
|
|
const handleBuy = async (tobuyId: number) => {
|
|
|
try {
|
|
|
const today = new Date().toISOString().substring(0, 10);
|
|
|
await toBuyApi.buy({tobuyId, ...newStock, lastUpdate: today});
|
|
|
fetchTasks(); // 削除後の買うもの一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.BUY_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 新規タスクを作成するハンドラー
|
|
|
* 入力されたタスク情報をAPIに送信して新規作成
|
|
|
* 作成後はダイアログを閉じ、入力内容をリセット
|
|
|
*/
|
|
|
const handleCreateTask = async () => {
|
|
|
try {
|
|
|
if (newToBuy.newAddition) {
|
|
|
newToBuy.stuffId = null;
|
|
|
}
|
|
|
console.log(newToBuy)
|
|
|
await toBuyApi.addToBuy(newToBuy);
|
|
|
setOpenDialog(false); // ダイアログを閉じる
|
|
|
setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
|
|
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_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>
|
|
|
{/* タスク一覧をマップして各タスクをリストアイテムとして表示 */}
|
|
|
{tobuys && tobuys.map((tobuy) => (
|
|
|
<ListItem
|
|
|
key={tobuy.tobuyId}
|
|
|
sx={{
|
|
|
bgcolor: 'background.paper',
|
|
|
mb: 1,
|
|
|
borderRadius: 1,
|
|
|
boxShadow: 1,
|
|
|
}}
|
|
|
>
|
|
|
{/* タスク完了状態を切り替えるチェックボックス */}
|
|
|
{/*}
|
|
|
<Checkbox
|
|
|
checked={task.completed}
|
|
|
onChange={() => handleToggleComplete(task.id)}
|
|
|
/>
|
|
|
*/}
|
|
|
{/* タスクのタイトルと説明 - 完了状態に応じて取り消し線を表示 */}
|
|
|
<ListItemText
|
|
|
primary={`${tobuy.stuffName} × ${tobuy.amount}`}
|
|
|
//secondary={tobuy.amount}
|
|
|
sx={{
|
|
|
textDecoration: false ? 'line-through' : 'none',
|
|
|
}}
|
|
|
/>
|
|
|
{/* 買い物リスト:食材情報記入ボタン */}
|
|
|
<ListItemSecondaryAction>
|
|
|
<Tooltip title="食材情報追加">
|
|
|
<IconButton
|
|
|
edge="end"
|
|
|
aria-label="食材情報追加"
|
|
|
onClick={() => {
|
|
|
setOpenInfoDialog(true)
|
|
|
setSelectedTask(tobuy.tobuyId)
|
|
|
// handleDeleteTask(tobuy.tobuyId)
|
|
|
}}
|
|
|
>
|
|
|
<ShoppingBasketIcon />
|
|
|
</IconButton>
|
|
|
</Tooltip>
|
|
|
{/* 買い物リスト:食材削除ボタン */}
|
|
|
<Tooltip title="項目を削除"
|
|
|
componentsProps={{
|
|
|
tooltip: {
|
|
|
sx: {
|
|
|
backgroundColor: "white",
|
|
|
color: "red",
|
|
|
fontSize: "0.8rem",
|
|
|
padding: "6px",
|
|
|
borderRadius: "6px",
|
|
|
},
|
|
|
},
|
|
|
}}
|
|
|
>
|
|
|
|
|
|
<IconButton
|
|
|
edge="end"
|
|
|
aria-label="delete"
|
|
|
onClick={() => handleDeleteTask(tobuy.tobuyId)}
|
|
|
>
|
|
|
<DeleteIcon />
|
|
|
</IconButton>
|
|
|
</Tooltip>
|
|
|
</ListItemSecondaryAction>
|
|
|
|
|
|
</ListItem>
|
|
|
))}
|
|
|
</List>
|
|
|
</div>
|
|
|
{/* 新規材料作成ボタン - 画面下部に固定表示 */}
|
|
|
<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}>
|
|
|
<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: "100%", marginBottom: 2 }}>
|
|
|
<InputLabel id="demo-simple-select-label">材料名(選択)</InputLabel>
|
|
|
<Select
|
|
|
labelId="demo-simple-select-label"
|
|
|
value={newToBuy.stuffId}
|
|
|
onChange={(e) => setNewToBuy({ ...newToBuy, stuffId: Number(e.target.value) })}
|
|
|
>
|
|
|
{stuffs.map((stuff) => (
|
|
|
<MenuItem key={stuff.stuffId} value={stuff.stuffId}>
|
|
|
{stuff.stuffName}
|
|
|
</MenuItem>
|
|
|
))}
|
|
|
</Select>
|
|
|
</FormControl>}
|
|
|
|
|
|
{/* タスクタイトル入力フィールド */}
|
|
|
{newToBuy.newAddition && <TextField
|
|
|
autoFocus
|
|
|
margin="dense"
|
|
|
label="材料名"
|
|
|
fullWidth
|
|
|
value={newToBuy.stuffName}
|
|
|
onChange={(e) => setNewToBuy({ ...newToBuy, stuffName: 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="価格"
|
|
|
fullWidth
|
|
|
value={newStock.price}
|
|
|
onChange={(e) => setNewStock({...newStock, price: parseInt(e.target.value)})}
|
|
|
/>
|
|
|
{/* 消費・賞味期限入力フィールド */}
|
|
|
<TextField
|
|
|
margin="dense"
|
|
|
label="消費・賞味期限(yyyy/MM/dd)"
|
|
|
fullWidth
|
|
|
multiline
|
|
|
value={newStock.expDate}
|
|
|
onChange={(e) => setNewStock({...newStock, expDate: e.target.value})}
|
|
|
/>
|
|
|
{/* 購入日入力フィールド */}
|
|
|
<TextField
|
|
|
margin="dense"
|
|
|
label="購入日(yyyy/MM/dd)"
|
|
|
fullWidth
|
|
|
multiline
|
|
|
value={newStock.buyDate}
|
|
|
onChange={(e) => setNewStock({...newStock, buyDate: e.target.value})}
|
|
|
/>
|
|
|
</Box>
|
|
|
</DialogContent>
|
|
|
<DialogActions>
|
|
|
<Button onClick={() => setOpenInfoDialog(false)}>キャンセル</Button>
|
|
|
<Button onClick={() => {
|
|
|
if (selectedTask) {
|
|
|
handleBuy(selectedTask)
|
|
|
setOpenInfoDialog(false)
|
|
|
setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
|
|
|
}
|
|
|
}}
|
|
|
variant="contained">
|
|
|
登録
|
|
|
</Button>
|
|
|
</DialogActions>
|
|
|
</Dialog>
|
|
|
</Container>
|
|
|
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default TaskListPage; |