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/AddRecipe.tsx

393 lines
14 KiB

/**
* テストページコンポーネント
* 白紙の状態で表示されるテスト用のページ
*/
import React, { useState, useEffect } from 'react';
import {
Typography,
Tooltip,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
IconButton,
Fab,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Button,
Checkbox,
FormControlLabel,
Box,
Container,
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon, Edit as EditIcon, Save as SaveIcon, ListAlt as ListAltIcon
} from '@mui/icons-material';
import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
import { StuffAndCategoryAndAmount } from '../types/types';
import EditAmountDialog from '../components/EditAmountDialog';
import { recipeApi, toBuyApi, stockApi } from '../services/api';
import { useNavigate, useParams } from 'react-router-dom';
import MessageAlert from '../components/MessageAlert';
import { useMessage } from '../components/MessageContext';
import { RECIPE_MESSAGES } from '../constants/normalMessages';
import { RECIPE_ERRORS } from '../constants/errorMessages';
const AddRecipe: React.FC = () => {
const { recipeId: recipeIdStr } = useParams();
const recipeId = recipeIdStr ? parseInt(recipeIdStr) : null
const navigate = useNavigate();
// 編集時,既存情報を読み込んだかどうか
const [recipeLoaded, setRecipeLoaded] = useState(false);
// 何人分かを格納する
const [numOfPeaple, setNumOfPeaple] = useState<number>(1);
const [openNumOfPeapleDialog, setOpenNumOfPeapleDialog] = useState(false);
// 料理名,説明
const [recipeName, setRecipeName] = useState<string>('');
const [recipeSummary, setRecipeSummary] = useState<string>('');
// 材料リスト
const [items, setItems] = useState<StuffAndCategoryAndAmount[]>([]);
// 材料追加作成ダイアログの表示状態
const [openAddDialog, setOpenAddDialog] = useState(false);
// 材料追加作成ダイアログの表示状態
const [openAmountDialog, setOpenAmountDialog] = useState(false);
// 新規アイテムの入力内容
const emptyItem: StuffAndCategoryAndAmount = { stuffId: null, stuffName: '', category: '', amount: 1 }
const [newItem, setNewItem] = useState<StuffAndCategoryAndAmount>(emptyItem);
// 編集しているアイテム
const [editingItem, setEditingItem] = useState<StuffAndCategoryAndAmount>(emptyItem);
const [editingItemIdx, setEditingItemIdx] = useState(0);
//削除確認ダイアログの表示状態
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
// チェックボックスが押されたかどうか
const [checked, setChecked] = useState(false);
// エラーメッセージ表示
const { showErrorMessage, showInfoMessage, showSuccessMessage } = useMessage();
const loadRecipe = async () => {
if (recipeId && !recipeLoaded) {
const recipe = await recipeApi.getById(recipeId);
console.log('loaded recipe=', recipe)
setRecipeName(recipe.recipeName)
setRecipeSummary(recipe.summary)
setItems(recipe.stuffAndAmountArray)
setRecipeLoaded(true)
}
}
const handleSaveRecipe = async () => {
if (!recipeName) {
showErrorMessage('レシピ名が入力されていません!')
// console.log("yes1");
return false;
}
if (!items.length) {
showErrorMessage('材料が追加されていません!')
// console.log("yes2");
return false;
}
try {
if (!recipeId) {
// console.log("yes3");
// 新規追加
const response = await recipeApi.addRecipe({
recipeName,
summary: recipeSummary,
stuffAndAmountArray: items,
})
return response.recipeId;
}
const response = await recipeApi.updateRecipe({
recipeId,
recipeName,
summary: recipeSummary,
stuffAndAmountArray: items,
})
} catch {
showErrorMessage('レシピの送信に失敗しました。同じ料理名が存在する可能性があります。');
return false;
}
// console.log("yes4");
return recipeId;
}
const checkRecipeAndItems = async () => {
if (!recipeName) {
showErrorMessage(RECIPE_ERRORS.INVALID_RECIPE_NAME);
// console.log("yes1");
return false;
}
if (!items.length) {
showErrorMessage(RECIPE_ERRORS.NO_STUFFS);
// console.log("yes2");
return false;
}
return true;
}
const handleSubmit = async () => {
const recipeId = await handleSaveRecipe();
if (!recipeId) return;
showSuccessMessage(RECIPE_MESSAGES.SAVED);
navigate('/recipeList');
}
const handleSubmitAndAddToBuy = async () => {
// console.log("too");
console.log('recipeName:', recipeName);
const recipeId = await handleSaveRecipe();
// console.log("before");
if (!recipeId) return false;
// console.log("ds");
if (!numOfPeaple) {
showErrorMessage(RECIPE_ERRORS.INVALID_SERVINGS);
// console.log("yes2");
return false;
}
const finalAddResult = await toBuyApi.addByRecipe(recipeId, numOfPeaple, checked);
console.log(finalAddResult)
// const recipeStuffInfo = (await recipeApi.getById(recipeId)).stuffAndAmountArray;
// const recipeStuffId = recipeStuffInfo.map(item => item.stuffId);
// console.log(recipeStuffId);
// const stockStuff = await stockApi.getStocks();
// const stockedStuffId = stockStuff.filter(stock => recipeStuff.stuffAndAmountArray).map(item => item.id);
// const stockedStuffAmountRecipe = recipeStuff.filter(recipe => (stockStuff.some(stuff => recipe.stuffId === stuff.stuffId)));
// console.log(stockedStuffAmountRecipe)
// const stockedStuffAmountStock = stockStuff.filter(stock => (stockedStuffAmountRecipe.some(stocked => stock.stuffId === stocked.stuffId)));
// recipeStuff.map(item => {
// await Promise.all(stockStuff.map(async stock => {
// if (item.stuffId == stock.stuffId)
// await toBuyApi.updateToBuy({stuffName: item.stuffName,
// amount: item.amount - stock.amount,
// tobuyId: item.tobuyId,
// stuffId: item.stuffId});
// }))
// });
if (finalAddResult.data.length === 0) {
showInfoMessage(RECIPE_MESSAGES.NO_ADDITIONALS);
}
else {
showSuccessMessage(RECIPE_MESSAGES.SAVED_AND_ADDED);
}
navigate('/tasks');
}
const openNumOfPeopleDialog = async () => {
const check = await checkRecipeAndItems();
if (!check) return false;
setOpenNumOfPeapleDialog(true);
}
const cancelNumOfPeopleDialog = async () => {
const recipeId = await handleSaveRecipe();
if (!recipeId) return false;
setOpenNumOfPeapleDialog(false);
}
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
loadRecipe();
}, []);
return (
<div className="mainContainer">
{(recipeId && !recipeLoaded)
? <p>...</p>
:
<>
<div className="mainTitle">
<SoupKitchenIcon sx={{ marginRight: "0.5em" }} />
{!recipeId ? '料理の追加' : '料理の編集'}
</div>
<div style={{padding: '1rem'}}>
<div>
<TextField autoFocus margin="dense" label="料理名" fullWidth
value={recipeName} onChange={(e) => setRecipeName(e.target.value)}
/>
<TextField margin="dense" label="説明" fullWidth
value={recipeSummary} onChange={(e) => setRecipeSummary(e.target.value)}
/>
</div>
<h2 style={{ marginTop: "0.5em" }}>
</h2>
{/* すべての材料情報を表示 */}
{(!items || !items.length)
? (<p>+</p>)
: (<List>{items.map((item, index) => (
<ListItem
key={index}
sx={{
bgcolor: 'background.paper',
mb: 1,
borderRadius: 1,
boxShadow: 1,
}}
>
<ListItemText primary={item.stuffName} />
{/* 買い物リスト:食材情報記入ボタン */}
<ListItemSecondaryAction>
<Typography variant="body1" component="span" sx={{ marginRight: '1em' }}>
{`× ${item.amount}`}
</Typography>
{/* 買い物リスト:数量変更ボタン */}
<Tooltip title="数量変更">
<IconButton sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="数量変更"
onClick={() => {
setOpenAmountDialog(true)
setEditingItemIdx(index)
setEditingItem(item)
}}
>
<EditIcon />
</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={() => {
setOpenDeleteDialog(true)
setEditingItem(item)
setEditingItemIdx(index)
}}>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}</List>)}
<div style={{ position: "fixed", left: "80%", transform: 'translateX(-50%)', bottom: "10%" }}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="caption"></Typography>
</Box>
<Fab color="primary" onClick={() => setOpenAddDialog(true)}>
<AddIcon sx={{ fontSize: "1.5rem" }} />
</Fab>
</div>
<div style={{ position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px", whiteSpace: 'nowrap' }}>
<Button variant='contained' color="primary" onClick={handleSubmit} sx={{ marginRight: "1rem" }}>
<SaveIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} />
</Button>
<Button variant='contained' color="primary" onClick={openNumOfPeopleDialog}>
<ListAltIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} />
</Button>
</div>
{/* 新規材料追加ダイアログ */}
<AddStuffAmountDialog openDialog={openAddDialog} setOpenDialog={setOpenAddDialog} newItem={newItem} setNewItem={setNewItem}
onSubmit={() => {
console.log('newItem:', newItem);
setItems([...items, newItem]);
setOpenAddDialog(false);
}} />
{/* 削除ダイアログ */}
<Dialog open={openDeleteDialog} onClose={() => setOpenDeleteDialog(false)} disableScrollLock={true}
fullWidth
maxWidth="sm"
sx={{ overflow: "hidden" }}
>
<DialogTitle></DialogTitle>
<DialogContent>
{editingItem && (
<>
<Typography variant="h4">{editingItem.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={() => {
setItems([...items.slice(0, editingItemIdx), ...items.slice(editingItemIdx + 1)])
setOpenDeleteDialog(false); // 削除処理後にダイアログを閉じる
}}
style={{ marginTop: "10px" }} sx={{ mt: 3, mb: 2, left: '72%' }}></Button>
</>
)}
</DialogContent>
</Dialog>
{/* 数量変更ダイアログ */}
<EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog}
editingItem={editingItem}
setEditingItem={(v) => setEditingItem({ ...editingItem, ...v })}
onSubmit={() => {
setItems([...items.slice(0, editingItemIdx), editingItem, ...items.slice(editingItemIdx + 1)])
setOpenAmountDialog(false);
}} />
{/* 人数入力ダイアログ */}
<Dialog open={openNumOfPeapleDialog} onClose={() => setOpenNumOfPeapleDialog(false)} disableScrollLock={true}
style={{ width : '100%', position : 'fixed', left: '50%', transform: 'translateX(-50%)' }}
>
<Box display="flex" alignItems="center"
>
<DialogTitle sx={{ flexGrow: 1 }}></DialogTitle>
</Box>
<DialogContent>
<div>
{/* 人数入力フィールド */}
<TextField
margin="dense"
label="何人前"
fullWidth
value={numOfPeaple}
onChange={(e) => {
const value = e.target.value;
const parsedValue = parseInt(value, 10); // 数値に変換
if (/*!isNaN(parsedValue) && */ isNaN(parsedValue) || parsedValue >= 1) { //負数除外
setNumOfPeaple(parsedValue); // number型で保存
}
}}
sx={{ minWidth: "8px", width: "100%" }}
type="number"
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
/>
</div>
<FormControlLabel
control={<Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />}
label={<Typography sx={{ fontSize: '85%' }}></Typography>}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenNumOfPeapleDialog(false)}></Button>
<Button onClick={() => handleSubmitAndAddToBuy()} variant="contained"
style={{ width: '40%' }}
>
</Button>
</DialogActions>
</Dialog>
</div>
</>
}
</div>
);
};
export default AddRecipe;