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.
 
 
 
 
 
joint_exc/frontend/src/pages/TaskListPage.tsx

519 lines
18 KiB

/**
* タスク一覧を表示・管理するページコンポーネント
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供
*/
import React, { useState, useEffect } from 'react';
import { toBuyApi, stuffApi } from '../services/api';
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
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, Edit as EditIcon
} 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 [openAmountDialog, setOpenAmountDialog] = useState(false);
const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0);
const [selectedEditingTask, setSelectedEditingTask] = useState<ToBuy>({
tobuyId: 0,
stuffId: 0,
stuffName: "",
amount: 0,
shop: undefined,
});
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();
console.log(tobuys);
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);
}
};
/**
* 買い物リストの数量を変更するハンドラー
* 入力されたタスク情報をAPIに送信して変更
* 作成後はダイアログを閉じ、入力内容をリセット
*/
const handleUpdateTask = async () => {
try {
console.log(selectedEditingTask)
await toBuyApi.updateToBuy(selectedEditingTask);
setOpenAmountDialog(false); // ダイアログを閉じる
//setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
fetchTasks(); // 作成後のタスク一覧を再取得
} catch (error) {
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
}
};
// /**
// * クリックされたタスクの情報を取得するための関数
// * @param tobuyId
// * @returns tobuyIdが一致するtoBuy
// */
// const getToBuyDetails = (tobuyId: number): ToBuy => {
// console.log(tobuyId)
// const result = tobuys.find((toBuy) => toBuy.tobuyId === tobuyId);
// if(result === undefined){
// throw new Error(`tobuyId: ${tobuyId} に対応するデータが見つかりません`);
// }
// return result;
// };
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="数量変更"
sx={{ marginRight: 2 }}
onClick={() => {
setOpenAmountDialog(true)
setSelectedEditingTask(tobuy)
}}
>
<EditIcon />
</IconButton>
</Tooltip>
{/* 買い物リスト:食材情報記入ボタン */}
<Tooltip title="食材情報追加">
<IconButton
edge="end"
aria-label="食材情報追加"
//sx={{ marginRight: 3 }}
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"
sx={{ marginLeft: 3 }}
aria-label="delete"
onClick={() => handleDeleteTask(tobuy.tobuyId)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</div>
{/* 新規材料作成ボタン - 画面下部に固定表示 */}
<Box sx={{ textAlign: 'center', position: 'fixed', bottom: 76, left: '40%', transform: 'translateX(-50%)' }}>
<Typography variant="caption" color="textSecondary">
</Typography>
</Box>
<Fab
color="primary"
sx={{ position: 'fixed', bottom: 16, left: '40%', transform: 'translateX(-50%)' }}
onClick={() => setOpenDialog(true)}
>
<AddIcon />
</Fab>
{/*新規料理追加ボタン - 画面下部に固定表示 */}
<Box sx={{ textAlign: 'center', position: 'fixed', bottom: 76, left: '60%', transform: 'translateX(-50%)' }}>
<Typography variant="caption" color="textSecondary">
</Typography>
</Box>
<Fab
color="primary"
sx={{ position: 'fixed', bottom: 16, left: '60%', transform: 'translateX(-50%)' }}
onClick={() => {
setOpenDialog(true);
//handleNavigate('/AddDishies1');
}}
//selected={isSelected('/test')}
>
<SoupKitchenIcon />
</Fab>
{/* 新規タスク作成ダイアログ */}
<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) && parsedValue >= 0) { //負数除外
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>
{/* 数量変更ダイアログ */}
<Dialog open={openAmountDialog} onClose={() => setOpenAmountDialog(false)} disableScrollLock={true}>
<Box display="flex" alignItems="center">
<DialogTitle sx={{ flexGrow: 1 }}></DialogTitle>
</Box>
<DialogContent>
<Box sx={{ pt: 1 }}>
{/* 材料名表示 */}
<TextField
autoFocus
margin="dense"
label="材料名"
fullWidth
value={selectedEditingTask.stuffName}
disabled
sx={{ marginBottom: 2 }}
/>
{/* 数量入力フィールド */}
<TextField
margin="dense"
label="数量"
fullWidth
value={selectedEditingTask.amount}
onChange={(e) => {
const value = e.target.value;
const parsedValue = parseInt(value, 10); // 数値に変換
if (!isNaN(parsedValue) && parsedValue >= 0) { //負数除外
setSelectedEditingTask({ ...selectedEditingTask, amount: parsedValue }); // number型で保存
}
}}
sx={{ width: "20%" }}
type="number"
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenAmountDialog(false)}></Button>
<Button onClick={handleUpdateTask} variant="contained">
</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default TaskListPage;