|
|
/**
|
|
|
* タスク一覧を表示・管理するページコンポーネント
|
|
|
* タスクの表示、作成、完了状態の切り替え、削除などの機能を提供
|
|
|
*/
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
import { stockApi, toBuyApi } from '../services/api';
|
|
|
import {
|
|
|
Container,
|
|
|
Typography,
|
|
|
Tooltip,
|
|
|
List,
|
|
|
ListItem,
|
|
|
ListItemText,
|
|
|
ListItemSecondaryAction,
|
|
|
IconButton,
|
|
|
Fab,
|
|
|
Box,
|
|
|
Dialog,
|
|
|
DialogTitle,
|
|
|
DialogContent,
|
|
|
Button,
|
|
|
} 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, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, StockHistory, /*Stock*/ } from '../types/types';
|
|
|
import { GENERAL_ERRORS, TOBUY_ERRORS } from '../constants/errorMessages';
|
|
|
import EditAmountDialog from '../components/EditAmountDialog';
|
|
|
import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
|
|
|
import BuyDialog from '../components/BuyDialog';
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
import DatePicker from 'react-datepicker';
|
|
|
import { useMessage } from '../components/MessageContext';
|
|
|
import StuffHistoryDialog from '../components/StuffHistoryDialog';
|
|
|
|
|
|
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
|
|
|
|
|
|
|
|
|
|
|
|
// 新規タスクの初期状態
|
|
|
const EMPTY_TOBUY: NewToBuy = {
|
|
|
stuffId: null,
|
|
|
stuffName: '',
|
|
|
amount: 1,
|
|
|
shop: '',
|
|
|
category: '',
|
|
|
}
|
|
|
|
|
|
const TaskListPage: React.FC = () => {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
// タスク一覧の状態管理
|
|
|
const [tobuys, setToBuys] = useState<ToBuy[]>([]);
|
|
|
|
|
|
// 買うものリストへの食材追加ダイアログの表示状態
|
|
|
const [openAddToBuyDialog, setOpenAddToBuyDialog] = useState(false);
|
|
|
|
|
|
//在庫登録ダイアログの表示状態
|
|
|
const [openInfoDialog, setOpenInfoDialog] = useState(false);
|
|
|
|
|
|
//数量変更ダイアログの表示状態
|
|
|
const [openAmountDialog, setOpenAmountDialog] = useState(false);
|
|
|
|
|
|
const [selectedToBuyId, setSelectedToBuyId] = useState<ToBuy["tobuyId"]>(0);
|
|
|
|
|
|
const [newStock, setNewStock] = useState<NewStock>({
|
|
|
amount: '', // 購入数量(ここではstring)
|
|
|
price: '', // ここではstring
|
|
|
shop: '',
|
|
|
buyDate: new Date().toISOString(),
|
|
|
expDate: '',
|
|
|
});
|
|
|
|
|
|
//削除確認ダイアログの表示状態
|
|
|
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
|
|
|
|
|
const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0);
|
|
|
|
|
|
// 編集対象の項目
|
|
|
const [editingItem, setEditingItem] = useState<ToBuy>({
|
|
|
tobuyId: 0,
|
|
|
stuffId: 0,
|
|
|
stuffName: "",
|
|
|
amount: 0,
|
|
|
shop: undefined,
|
|
|
});
|
|
|
|
|
|
const [newToBuy, setNewToBuy] = useState<StuffAndCategoryAndAmount>(EMPTY_TOBUY);
|
|
|
const [selectedDeleteTask, setSelectedDeleteTask] = useState<ToBuy>({
|
|
|
tobuyId: 0,
|
|
|
stuffId: 0,
|
|
|
stuffName: "",
|
|
|
amount: 0,
|
|
|
shop: undefined,
|
|
|
});
|
|
|
|
|
|
const { showErrorMessage } = useMessage();
|
|
|
|
|
|
// コンポーネントマウント時にタスク一覧を取得
|
|
|
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);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// /**
|
|
|
// * タスクの完了状態を切り替えるハンドラー
|
|
|
// * 対象タスクの完了状態を反転させて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 handleDeleteToBuy = async (toBuyId: number) => {
|
|
|
try {
|
|
|
await toBuyApi.deleteToBuy(toBuyId);
|
|
|
fetchTasks(); // 削除後の買うもの一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.DELETE_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 買うものリストへの材料追加を行う
|
|
|
* 入力された買うもの情報をAPIに送信して新規作成
|
|
|
* 作成後はダイアログを閉じ、入力内容をリセット
|
|
|
*/
|
|
|
const handleAddNewToBuy = async () => {
|
|
|
try {
|
|
|
if (isNaN(newToBuy.amount)) {
|
|
|
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
newToBuy.stuffName = newToBuy.stuffName.trim(); // 材料名の前後の空白を削除
|
|
|
|
|
|
if (!newToBuy.stuffName) {
|
|
|
showErrorMessage(GENERAL_ERRORS.INVALID_STUFF_NAME);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
console.log(newToBuy)
|
|
|
await toBuyApi.addToBuy(newToBuy);
|
|
|
setOpenAddToBuyDialog(false); // ダイアログを閉じる
|
|
|
setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
|
|
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 買い物リストの数量を変更するハンドラー
|
|
|
* 入力されたタスク情報をAPIに送信して変更
|
|
|
* 作成後はダイアログを閉じ、入力内容をリセット
|
|
|
*/
|
|
|
const handleUpdateNewToBuy = async () => {
|
|
|
try {
|
|
|
if (isNaN(editingItem.amount)) {
|
|
|
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
console.log(editingItem)
|
|
|
await toBuyApi.updateToBuy(editingItem);
|
|
|
setOpenAmountDialog(false); // ダイアログを閉じる
|
|
|
//setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
|
|
|
fetchTasks(); // 作成後のタスク一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 買うものリストの在庫登録(購入処理)を行うハンドラー
|
|
|
*/
|
|
|
const handleBuy = async () => {
|
|
|
try {
|
|
|
const today = new Date().toISOString().substring(0, 10);
|
|
|
const parsedPrice = parseInt(newStock.price, 10);
|
|
|
console.log("newPrice:", newStock.price)
|
|
|
console.log("parsedPrice: ", parsedPrice)
|
|
|
if (isNaN(parsedPrice)) {
|
|
|
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE)
|
|
|
return
|
|
|
//setNewStock({ ...newStock, price: parsedPrice });
|
|
|
}
|
|
|
const amount = parseInt(newStock.amount, 10);
|
|
|
if (isNaN(amount)) {
|
|
|
showErrorMessage('購入数量が正しく入力されていません。')
|
|
|
return
|
|
|
}
|
|
|
await toBuyApi.buy({
|
|
|
tobuyId: selectedToBuyId,
|
|
|
...newStock,
|
|
|
amount,
|
|
|
price: parsedPrice,
|
|
|
lastUpdate: today
|
|
|
}); //データベースに送信
|
|
|
setOpenInfoDialog(false);
|
|
|
fetchTasks(); // 変更後後の買うもの一覧を再取得
|
|
|
} catch (error) {
|
|
|
console.error(`${TOBUY_ERRORS.BUY_FAILED}:`, error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
//履歴表示ダイアログ
|
|
|
const [openHistoryDialog, setOpenHistoryDialog] = useState(false);
|
|
|
const [historyTobuy, setHistoryTobuy] = useState<ToBuy | null>(null);
|
|
|
const [stockHistories, setStockHistories] = useState<StockHistory[] | null>(null);
|
|
|
|
|
|
const handleShowHistories = async (tobuy: ToBuy) => {
|
|
|
console.log('handleShowHistories:', tobuy);
|
|
|
try {
|
|
|
setHistoryTobuy(tobuy);
|
|
|
setStockHistories(await stockApi.getHistories(tobuy.stuffId));
|
|
|
setOpenHistoryDialog(true);
|
|
|
} catch {
|
|
|
showErrorMessage("履歴の読み込みに失敗しました。");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
}}
|
|
|
>
|
|
|
{/* 食材名 */}
|
|
|
<ListItemText
|
|
|
primary={tobuy.stuffName}
|
|
|
onClick={() => handleShowHistories(tobuy)}
|
|
|
/>
|
|
|
{/* 買い物リスト:食材情報記入ボタン */}
|
|
|
<ListItemSecondaryAction>
|
|
|
<Typography variant="body1" component="span" sx={{ marginRight: '1em' }}>
|
|
|
{`× ${tobuy.amount}`}
|
|
|
</Typography>
|
|
|
{/* 買い物リスト:数量変更ボタン */}
|
|
|
<Tooltip title="数量変更">
|
|
|
<IconButton sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="数量変更"
|
|
|
onClick={() => {
|
|
|
setOpenAmountDialog(true)
|
|
|
setEditingItem(tobuy)
|
|
|
}}
|
|
|
>
|
|
|
<EditIcon />
|
|
|
</IconButton>
|
|
|
</Tooltip>
|
|
|
{/* 買い物リスト:食材情報記入ボタン */}
|
|
|
<Tooltip title="購入情報を記入">
|
|
|
<IconButton color="primary" sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="購入情報を記入"
|
|
|
onClick={() => {
|
|
|
setOpenInfoDialog(true)
|
|
|
setEditingItem(tobuy)
|
|
|
setNewStock({ ...newStock, amount: String(tobuy.amount) });
|
|
|
setSelectedToBuyId(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={{ marginRight: 0, marginLeft: 0 }}
|
|
|
aria-label="delete"
|
|
|
onClick={() => {//handleDeleteTask(tobuy.tobuyId)
|
|
|
setSelectedDeleteTask(tobuy)
|
|
|
setOpenDeleteDialog(true)
|
|
|
}
|
|
|
}
|
|
|
>
|
|
|
<DeleteIcon />
|
|
|
</IconButton>
|
|
|
</Tooltip>
|
|
|
|
|
|
</ListItemSecondaryAction>
|
|
|
|
|
|
</ListItem>
|
|
|
))}
|
|
|
</List>
|
|
|
</div>
|
|
|
{/* 新規材料作成ボタン - 画面下部に固定表示 */}
|
|
|
<Box sx={{ textAlign: 'center', position: 'fixed', bottom: 66, left: '80%', transform: 'translateX(-50%)' }}>
|
|
|
<Typography variant="caption" color="textSecondary">
|
|
|
材料の追加
|
|
|
</Typography>
|
|
|
</Box>
|
|
|
<Fab
|
|
|
color="primary"
|
|
|
sx={{ position: 'fixed', bottom: 90, left: '80%', transform: 'translateX(-50%)' }}
|
|
|
onClick={() => setOpenAddToBuyDialog(true)}
|
|
|
>
|
|
|
<AddIcon />
|
|
|
</Fab>
|
|
|
|
|
|
{/*新規料理追加ボタン - 画面下部に固定表示 */}
|
|
|
{/* <Box sx={{ textAlign: 'center', position: 'fixed', bottom: 66, left: '80%', transform: 'translateX(-50%)' }}>
|
|
|
<Typography variant="caption" color="textSecondary">
|
|
|
料理から追加
|
|
|
</Typography>
|
|
|
</Box>
|
|
|
<Fab
|
|
|
color="primary"
|
|
|
sx={{ position: 'fixed', bottom: 90, left: '80%', transform: 'translateX(-50%)' }}
|
|
|
onClick={() => {
|
|
|
// setOpenAddToBuyDialog(true);
|
|
|
navigate('/RecipeList');
|
|
|
}}
|
|
|
//selected={isSelected('/test')}
|
|
|
>
|
|
|
<SoupKitchenIcon />
|
|
|
</Fab> */}
|
|
|
|
|
|
|
|
|
{/* 買うものリストへの材料追加ダイアログ */}
|
|
|
<AddStuffAmountDialog openDialog={openAddToBuyDialog} setOpenDialog={setOpenAddToBuyDialog} newItem={newToBuy} setNewItem={setNewToBuy} onSubmit={handleAddNewToBuy} />
|
|
|
|
|
|
{/* 購入処理(在庫登録)のための数値入力ダイアログ */}
|
|
|
<BuyDialog openDialog={openInfoDialog} setOpenDialog={setOpenInfoDialog} stuffName={editingItem.stuffName} newStock={newStock} setNewStock={setNewStock} onSubmit={handleBuy} />
|
|
|
|
|
|
{/* 数量変更ダイアログ */}
|
|
|
<EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog}
|
|
|
editingItem={editingItem}
|
|
|
setEditingItem={(v) => setEditingItem({ ...editingItem, ...v })}
|
|
|
onSubmit={handleUpdateNewToBuy} />
|
|
|
|
|
|
{/* 削除ダイアログ */}
|
|
|
<Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)} disableScrollLock={true}
|
|
|
fullWidth
|
|
|
maxWidth="sm"
|
|
|
sx={{ overflow: "hidden" }}
|
|
|
>
|
|
|
<DialogTitle>食材の削除</DialogTitle>
|
|
|
<DialogContent>
|
|
|
{selectedDeleteTask && (
|
|
|
<>
|
|
|
<Typography variant="h4">{selectedDeleteTask.stuffName}を削除します。</Typography>
|
|
|
<Typography variant="body1" color="error">⚠️ 注意: 削除すると復元できません。</Typography>
|
|
|
<Button onClick={() => setOpenDeleteDialog(false)} sx={{ mt: 3, mb: 2, left: '70%' }}>キャンセル</Button>
|
|
|
<Button variant="contained" color="error" onClick={() => {
|
|
|
handleDeleteToBuy(selectedDeleteTask.tobuyId);
|
|
|
setOpenDeleteDialog(false); // 削除処理後にダイアログを閉じる
|
|
|
}}
|
|
|
style={{ marginTop: "10px" }} sx={{ mt: 3, mb: 2, left: '72%' }}>削除</Button>
|
|
|
</>
|
|
|
)}
|
|
|
</DialogContent>
|
|
|
</Dialog>
|
|
|
|
|
|
{/* 履歴表示ダイアログ */}
|
|
|
{
|
|
|
(historyTobuy !== null && stockHistories !== null) &&
|
|
|
<StuffHistoryDialog openDialog={openHistoryDialog} setOpenDialog={setOpenHistoryDialog} stuffName={historyTobuy.stuffName} stockHistories={stockHistories} />
|
|
|
}
|
|
|
|
|
|
</Container>
|
|
|
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default TaskListPage; |