Compare commits

...

23 Commits

Author SHA1 Message Date
Masaharu.Kato 0e5e226755 エラーハンドリングを追加 4 months ago
Masaharu.Kato c790bebb92 在庫追加時のエラーを実装 4 months ago
Masaharu.Kato 6ae78c5d43 在庫追加画面の修正 4 months ago
Masaharu.Kato 00f8401ed6 買うものリストへのレシピ追加画面の実装 4 months ago
Masaharu.Kato 403d25a99c Merge remote-tracking branch 'origin/feature-dishuiandcolor' into feature-frontend-design-fix 4 months ago
Masaharu.Kato c475afe2ae 料理リストで空白のものが表示されるのを修正 4 months ago
Amagasu c90d99c11e Merge remote-tracking branch 'origin/feature-frontend-design-fix' into feature-dishuiandcolor 4 months ago
Masaharu.Kato c4fb41e321 レシピ一覧・編集画面の修正 4 months ago
Masaharu.Kato 7ec94b45f2 Merge remote-tracking branch 'origin/feature_frontend_dishList_ui' into feature-frontend-design-fix 4 months ago
Amagasu c3594a7575 Acreated ddByRecipeDialog 4 months ago
Masaharu.Kato 70cdfdb9d7 Merge remote-tracking branch 'origin/develop-frontend' into feature-frontend-design-fix 4 months ago
Amagasu 21bdbace3c Merge remote-tracking branch 'origin/develop-backend' into feature_frontend_dishList_ui 4 months ago
Amagasu dbd86b5a29 wip AddByRecipeDialog 4 months ago
masato.fujita cf5e7b3ddd 新しい料理を追加ボタンのUI変更、用意できる人数によって表示内容を変更 4 months ago
Masaharu.Kato 9c27f37cb5 賞味期限をオプション化 4 months ago
Masaharu.Kato 4285d1ea24 Merge remote-tracking branch 'origin/feature-frontend-tobuy-error' into feature-frontend-dateselect-fix 4 months ago
Masaharu.Kato a48247def5 購入日・賞味期限コンポーネントの実装 4 months ago
Masaharu.Kato 615f9bd607 Merge branch 'develop' into develop-frontend 4 months ago
Masaharu.Kato fa5cdd5056 Merge remote-tracking branch 'origin/develop-backend' into feature-backend-stock-service 4 months ago
Masaharu.Kato 0a31b9c6fe 料理情報取得時にも最大調理可能数を返すように修正 4 months ago
masato.fujita c490af7489 コンソールundefined問題 4 months ago
akito.nishiwaki d72e746991 Merge remote-tracking branch 'origin/develop-frontend' into feature-frontend-register 4 months ago
akito.nishiwaki 4c14bcde41 OS標準のカレンダー設定 4 months ago
  1. 9
      backend/src/main/java/com/example/todoapp/controller/RecipesController.java
  2. 6
      backend/src/main/java/com/example/todoapp/dto/RecipeDetailDTO.java
  3. 2
      backend/src/main/java/com/example/todoapp/model/Stocks.java
  4. 4
      backend/src/main/java/com/example/todoapp/service/RecipeService.java
  5. 160
      frontend/src/components/AddByRecipeDialog.tsx
  6. 230
      frontend/src/components/BuyDialog.tsx
  7. 80
      frontend/src/components/BuyExpDateSelect.tsx
  8. 1
      frontend/src/components/Layout.tsx
  9. 4
      frontend/src/components/StuffHistoryDialog.tsx
  10. 7
      frontend/src/constants/normalMessages.ts
  11. 65
      frontend/src/pages/AddRecipe.tsx
  12. 131
      frontend/src/pages/RecipeList.tsx
  13. 273
      frontend/src/pages/StockPage.tsx
  14. 28
      frontend/src/pages/TaskListPage.tsx
  15. 46
      frontend/src/services/api.ts
  16. 23
      frontend/src/types/types.ts

@ -99,8 +99,12 @@ public class RecipesController {
public ResponseEntity<RecipeDetailDTO> getRecipeById( public ResponseEntity<RecipeDetailDTO> getRecipeById(
Authentication authentication, Authentication authentication,
@RequestParam Long recipeId) { @RequestParam Long recipeId) {
recipeService.getRecipeDetailsById(recipeId);
return ResponseEntity.ok(recipeService.getRecipeDetailsById(recipeId)); User user = userRepository.findByUsername(authentication.getName())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
RecipeDetailDTO response = recipeService.getRecipeDetailsById(user.getId(), recipeId);
return ResponseEntity.ok(response);
} }
/** /**
@ -120,7 +124,6 @@ public class RecipesController {
} else { } else {
response.put("result", false); response.put("result", false);
response.put("message", "編集に失敗しました"); response.put("message", "編集に失敗しました");
} }
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }

@ -30,6 +30,12 @@ public class RecipeDetailDTO {
* レシピの簡単な説明文 * レシピの簡単な説明文
*/ */
private String summary; private String summary;
/**
* 最大調理可能数
* そのレシピを何人分調理可能か
*/
private int maxServings;
/** /**
* 食材リスト * 食材リスト

@ -100,7 +100,7 @@ public class Stocks {
/** /**
* 賞味期限 * 賞味期限
*/ */
@Column(nullable = false) @Column(nullable = true)
private LocalDate expDate; private LocalDate expDate;
} }

@ -27,6 +27,7 @@ import com.example.todoapp.model.Stuffs;
import com.example.todoapp.repository.RecipeStuffsRepository; import com.example.todoapp.repository.RecipeStuffsRepository;
import com.example.todoapp.repository.RecipesRepository; import com.example.todoapp.repository.RecipesRepository;
import com.example.todoapp.repository.StuffsRepository; import com.example.todoapp.repository.StuffsRepository;
import com.example.todoapp.repository.UserRepository;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@ -140,7 +141,7 @@ public class RecipeService {
* @return レシピ詳細情報レシピ基本情報と関連食材情報 * @return レシピ詳細情報レシピ基本情報と関連食材情報
* @throws RuntimeException レシピが見つからない場合 * @throws RuntimeException レシピが見つからない場合
*/ */
public RecipeDetailDTO getRecipeDetailsById(Long recipeId) { public RecipeDetailDTO getRecipeDetailsById(Long userId, Long recipeId) {
Recipes recipe = recipesRepository.findById(recipeId) Recipes recipe = recipesRepository.findById(recipeId)
.orElseThrow(() -> new RuntimeException("レシピが見つかりません")); .orElseThrow(() -> new RuntimeException("レシピが見つかりません"));
@ -162,6 +163,7 @@ public class RecipeService {
dto.setRecipeId(recipe.getRecipeId()); dto.setRecipeId(recipe.getRecipeId());
dto.setRecipeName(recipe.getRecipeName()); dto.setRecipeName(recipe.getRecipeName());
dto.setSummary(recipe.getSummary()); dto.setSummary(recipe.getSummary());
dto.setMaxServings(getRecipeMaxServings(userId, recipe));
dto.setStuffAndAmountArray(stuffList); dto.setStuffAndAmountArray(stuffList);
return dto; return dto;

@ -0,0 +1,160 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
List,
ListItem,
ListItemText,
ListItemSecondaryAction,
Typography,
TextField,
FormControlLabel,
Checkbox,
} from '@mui/material';
import { toBuyApi } from '../services/api';
import { recipeApi } from '../services/api';
import CloseIcon from '@mui/icons-material/Close';
import { RecipeDetailWithId, StuffNameAndAmount } from '../types/types';
import { GENERAL_ERRORS } from '../constants/errorMessages';
import { useNavigate } from 'react-router-dom';
import { useMessage } from './MessageContext';
const AddByRecipeDialog = ({
openDialog,
setOpenDialog,
recipeId,
numOfPeople,
setNumOfPeaple,
checked,
setChecked
}: {
openDialog: boolean,
setOpenDialog: (open: boolean) => void,
recipeId: number,
numOfPeople: string,
setNumOfPeaple: (num: string) => void,
checked: boolean,
setChecked: (checked: boolean) => void
}) => {
const [recipe, setRecipe] = useState<RecipeDetailWithId>();
useEffect(() => {
console.log("called AddByRecipeDialog useEffect recipeId: ", recipeId);
if (recipeId) {
const fetchRecipe = async () => {
console.log("Fetching recipe with ID:", recipeId);
setRecipe(await recipeApi.getById(recipeId));
};
fetchRecipe();
}
}, [recipeId]);
const navigate = useNavigate();
// エラーメッセージ表示
const { showErrorMessage, showSuccessMessage, showInfoMessage } = useMessage();
return (
recipe ?
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '80vw', maxHeight: '80vh' } }}>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<CloseIcon
onClick={() => setOpenDialog(false)}
sx={{
cursor: 'pointer',
color: (theme) => theme.palette.grey[500],
}}
/>
</DialogTitle>
<DialogContent dividers sx={{ padding: 2 }}>
<Typography variant="h3">
{recipe.recipeName}
</Typography>
<Typography>
({recipe.maxServings})
</Typography>
<div>
<strong>1:</strong>
<List>
{recipe.stuffAndAmountArray.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>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</div>
<div>
{/* 人数入力フィールド */}
<TextField
type="number"
margin="dense"
label="何人前"
fullWidth
defaultValue={1}
value={numOfPeople}
onChange={(e) => {
// const num = parseInt(e.target.value, 10);
// setNumOfPeaple(!isNaN(num) ? num : '');
setNumOfPeaple(e.target.value);
}}
sx={{ minWidth: "8px", width: "100%" }}
inputProps={{ inputMode: "numeric", min: 1, pattern: "[0-9]*" }} // ここで整数のみ許可
/>
</div>
<FormControlLabel
control={<Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />}
label={<Typography></Typography>}
/>
{/* 買うものリストに追加するボタン */}
<Box sx={{ mt: 2, textAlign: 'right' }}>
<Button
variant="contained"
color="primary"
onClick={async () => {
const num = parseInt(numOfPeople, 10);
if (!num || isNaN(num)) {
showErrorMessage('人数が正しく入力されていません。');
return;
}
const finalAddResult = await toBuyApi.addByRecipe(recipe.recipeId, num, checked);
setOpenDialog(false);
if (finalAddResult.data.length === 0) {
showInfoMessage('必要な食材が在庫にあったのでリストには追加されませんでした!');
} else {
showSuccessMessage('レシピが保存されて買うものリストに追加されました!');
}
navigate('/tasks');
}}
>
</Button>
</Box>
</DialogContent>
</Dialog>
: <></>
);
}
export default AddByRecipeDialog;

@ -1,159 +1,115 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
TextField, TextField,
Button, Button,
Box, Box,
} from '@mui/material'; } from '@mui/material';
import { NewStock } from '../types/types'; import { NewStock } from '../types/types';
import DatePicker, { registerLocale } from 'react-datepicker'; import BuyExpDateSelect from './BuyExpDateSelect';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート /*import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート*/
// 日付をyyyy-MM-dd形式で返す関数 /*// yyyy-MM-dd形式で返す関数
const formatDateLocal = (date: Date) => { const formatDateLocal = (date: Date) => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };*/
// 日本語ロケールを登録 // 日本語ロケールを登録
registerLocale('ja', ja); //registerLocale('ja', ja);
const BuyDialog = ({ const BuyDialog = ({
openDialog, openDialog,
setOpenDialog, setOpenDialog,
stuffName, stuffName,
newStock, newStock,
setNewStock, setNewStock,
onSubmit, onSubmit,
}: { }: {
openDialog: boolean, openDialog: boolean,
setOpenDialog: (open: boolean) => void, setOpenDialog: (open: boolean) => void,
stuffName: string, stuffName: string,
newStock: NewStock, newStock: NewStock,
setNewStock: (tobuy: NewStock) => void, setNewStock: (tobuy: NewStock) => void,
onSubmit: () => void, onSubmit: () => void,
}) => { }) => {
return ( return (
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }} <Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }}
> >
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogContent> <DialogContent>
<Box sx={{ pt: 1 }}> <Box sx={{ pt: 1 }}>
{/* 材料名表示 */} {/* 材料名表示 */}
<TextField <TextField
margin="dense" margin="dense"
label="材料名" label="材料名"
fullWidth fullWidth
value={stuffName} value={stuffName}
disabled disabled
sx={{ marginBottom: 2, marginTop: 2 }} sx={{ marginBottom: 2, marginTop: 2 }}
/> />
{/* 価格入力フィールド */} {/* 価格入力フィールド */}
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
label="価格" label="価格"
fullWidth fullWidth
value={newStock.price} value={newStock.price}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
if (/^\d*$/.test(value)) { if (/^\d*$/.test(value)) {
setNewStock({ ...newStock, price: value }) setNewStock({ ...newStock, price: value })
}; };
}} }}
sx={{ marginBottom: 2 }} sx={{ marginBottom: 2 }}
/> />
{/* 価格入力フィールド */} {/* 価格入力フィールド */}
<TextField <TextField
autoFocus autoFocus
margin="dense" margin="dense"
label="購入数量" label="購入数量"
fullWidth fullWidth
value={newStock.amount} value={newStock.amount}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
if (/^\d*$/.test(value)) { if (/^\d*$/.test(value)) {
setNewStock({ ...newStock, amount: value }) setNewStock({ ...newStock, amount: value })
}; };
}} }}
sx={{ marginBottom: 2 }} sx={{ marginBottom: 2 }}
/> />
{/* 購入日・消費期限を横並びに */} {/* 購入日・賞味期限入力 */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}> <BuyExpDateSelect newStock={newStock} setNewStock={({buyDate, expDate}) => setNewStock({...newStock, buyDate, expDate}) } />
{/* 購入日入力フィールド */}
<DatePicker
popperClassName="custom-datepicker-popper"
selected={newStock.buyDate ? new Date(newStock.buyDate) : null}
onChange={(date) =>
setNewStock({ ...newStock, buyDate: date ? formatDateLocal(date) : '' })
}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="dense"
label="購入日(yyyy/MM/dd)"
fullWidth
/>
}
isClearable
//withPortal // ← 他の文字との重なり対策
/>
{/*
<TextField
margin="dense"
label="購入日(yyyy/MM/dd)"
fullWidth
value={newStock.buyDate}
onChange={(e) => setNewStock({ ...newStock, buyDate: e.target.value })}
/>
*/}
{/* 消費・賞味期限入力フィールド */}
<DatePicker
popperClassName="custom-datepicker-popper"
selected={newStock.expDate ? new Date(newStock.expDate) : null}
onChange={(date) =>
setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' })
}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="dense"
label="消費・賞味期限(yyyy/MM/dd)"
fullWidth
/>
}
isClearable
//withPortal
/>
</Box>
{/* 購入店舗入力フィールド */}
<TextField
margin="dense"
label="店舗"
value={newStock.shop}
onChange={(e) => setNewStock({...newStock, shop: e.target.value})}
fullWidth
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}></Button>
<Button onClick={() => { onSubmit() }} variant="contained"></Button>
</DialogActions>
</Dialog>
) {/* 購入店舗入力フィールド */}
<TextField
margin="dense"
label="店舗"
value={newStock.shop}
onChange={(e) => setNewStock({ ...newStock, shop: e.target.value })}
fullWidth
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}></Button>
<Button onClick={() => { onSubmit() }} variant="contained"></Button>
</DialogActions>
</Dialog>
)
} }
export default BuyDialog; export default BuyDialog;

@ -0,0 +1,80 @@
import { Box, TextField } from "@mui/material"
// 日付文字列(時間を含む可能性がある)から日付部分だけを返す
const parseDate = ((date: string) => date ? date.substring(0, 10) : '')
// 今日の日付
const today = parseDate(new Date().toISOString());
// 日付文字列で早いほうを返す(空でないものは除く)
const validateBuyDate = ((buyDate: string) => {
// console.log('validateBuyDate:', buyDate)
if (!buyDate) { // 購入日が未設定の場合は今日の日付を入れておく
return today;
}
return parseDate(buyDate);
})
// 日付文字列で早いほうを返す(空でないものは除く)
const validateExpDate = ((buyDate: string, expDate: string | null) => {
// console.log('validateExpDate:', buyDate, expDate)
if (!expDate) { // 賞味期限が未設定の場合そのまま未設定にする
return '';
}
const buyDateParsed = validateBuyDate(buyDate);
const expDateParsed = parseDate(expDate);
// 購入日以降となるようにする
return buyDateParsed > expDateParsed ? buyDateParsed : expDateParsed;
})
const BuyExpDateSelect = ({
newStock,
setNewStock,
}: {
newStock: {buyDate: string, expDate: string | null},
setNewStock: (tobuy: {buyDate: string, expDate: string | null}) => void,
}) => {
{/* 購入日・消費期限を横並びに */ }
return <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
<TextField
margin="dense"
label="購入日"
type="date"
fullWidth
value={validateBuyDate(newStock.buyDate)}
onChange={(e) =>
setNewStock({ ...newStock, buyDate: e.target.value })
}
InputLabelProps={{ shrink: true }}
InputProps={{
inputProps: {
max: today,
}
}}
/>
{/* 消費・賞味期限入力フィールド */}
<TextField
margin="dense"
label="消費・賞味期限"
type="date"
fullWidth
value={validateExpDate(newStock.buyDate, newStock.expDate)}
onChange={(e) =>
setNewStock({ ...newStock, expDate: e.target.value ?? null })
}
InputLabelProps={{ shrink: true }}
InputProps={{
inputProps: {
min: validateBuyDate(newStock.buyDate),
}
}}
/>
</Box>
}
export default BuyExpDateSelect;

@ -101,6 +101,7 @@ const Layout: React.FC = () => {
const [msgType, setMsgType] = useState<AlertColor>('info'); const [msgType, setMsgType] = useState<AlertColor>('info');
const showMessage = (msg: string, sev: AlertColor) => { const showMessage = (msg: string, sev: AlertColor) => {
console.log('ShowMessage:', msg, sev);
setMsgText(msg); setMsgText(msg);
setMsgType(sev); setMsgType(sev);
setMsgOpen(true); setMsgOpen(true);

@ -32,7 +32,7 @@ const StuffHistoryDialog = ({
}) => { }) => {
return ( return (
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '300px', maxHeight: '80vh' } }}> <Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '90vw', maxHeight: '80vh' } }}>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
: {stuffName} : {stuffName}
<IconButton <IconButton
@ -68,7 +68,7 @@ const StuffHistoryDialog = ({
{/* 各ヘッダーセルに white-space: nowrap; を適用 */} {/* 各ヘッダーセルに white-space: nowrap; を適用 */}
<TableCell sx={{ whiteSpace: 'nowrap' }}></TableCell> <TableCell sx={{ whiteSpace: 'nowrap' }}></TableCell>
{/* 「購入店舗」ヘッダーも nowrap にし、minWidth でスクロールを考慮 */} {/* 「購入店舗」ヘッダーも nowrap にし、minWidth でスクロールを考慮 */}
<TableCell sx={{ whiteSpace: 'nowrap', minWidth: '150px' }}></TableCell> <TableCell sx={{ whiteSpace: 'nowrap', minWidth: '120px' }}></TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell> <TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell> <TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell>
</TableRow> </TableRow>

@ -14,3 +14,10 @@ export const RECIPE_MESSAGES = {
SAVED_AND_ADDED: 'レシピが保存されて買うものリストに追加されました!', SAVED_AND_ADDED: 'レシピが保存されて買うものリストに追加されました!',
NO_ADDITIONALS: '必要な食材が在庫にあったのでリストには追加されませんでした。', NO_ADDITIONALS: '必要な食材が在庫にあったのでリストには追加されませんでした。',
} }
// 在庫リスト
export const STOCK_MESSAGES = {
CREATE_OK: '在庫リストに追加されました!',
UPDATE_OK: '在庫情報が編集されました!',
DELETE_OK: '在庫リストから削除されました!',
};

@ -193,9 +193,13 @@ const AddRecipe: React.FC = () => {
} }
const cancelNumOfPeopleDialog = async () => { const cancelNumOfPeopleDialog = async () => {
const recipeId = await handleSaveRecipe(); // const allRecipes = await recipeApi.getAllRecipes();
if (!recipeId) return false; // const allRecipesId = allRecipes.map(recipe => recipe.recipeId);
setOpenNumOfPeapleDialog(false); // const allStocks = await stockApi.getStocks();
// const allStuffIdAndAmount = [allStocks.map(stock => stock.stuffId), allStocks.map(stock => stock.amount)];
// Promise.all(allRecipesId.map(async recipe => {
// (await recipeApi.getById(recipe)
// }))
} }
// コンポーネントマウント時にタスク一覧を取得 // コンポーネントマウント時にタスク一覧を取得
@ -278,12 +282,10 @@ const AddRecipe: React.FC = () => {
))}</List>)} ))}</List>)}
<div style={{ position: "fixed", left: "80%", transform: 'translateX(-50%)', bottom: "10%" }}> <div style={{ position: "fixed", left: "80%", transform: 'translateX(-50%)', bottom: "10%" }}>
<Box sx={{ textAlign: 'center' }}> {/* <Box sx={{ textAlign: 'center' }}>
<Typography variant="caption"></Typography> <Typography variant="caption"></Typography>
</Box> </Box> */}
<Fab color="primary" onClick={() => setOpenAddDialog(true)}> <Fab color="primary" onClick={() => setOpenAddDialog(true)}>
<AddIcon sx={{ fontSize: "1.5rem" }} /> <AddIcon sx={{ fontSize: "1.5rem" }} />
</Fab> </Fab>
@ -292,11 +294,7 @@ const AddRecipe: React.FC = () => {
<div style={{ position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px", whiteSpace: 'nowrap' }}> <div style={{ position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px", whiteSpace: 'nowrap' }}>
<Button variant='contained' color="primary" onClick={handleSubmit} sx={{ marginRight: "1rem" }}> <Button variant='contained' color="primary" onClick={handleSubmit} sx={{ marginRight: "1rem" }}>
<SaveIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} /> <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> </Button>
</div> </div>
@ -340,49 +338,6 @@ const AddRecipe: React.FC = () => {
setOpenAmountDialog(false); 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>
</> </>
} }

@ -28,10 +28,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import '../App.css'; import '../App.css';
import { import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon SoupKitchen as SoupKitchenIcon, Close as CloseIcon, TaskAlt as TaskAltIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types'; import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import AddByRecipeDialog from '../components/AddByRecipeDialog';
import { useMessage } from '../components/MessageContext'; import { useMessage } from '../components/MessageContext';
import { RECIPE_ERRORS } from '../constants/errorMessages'; import { RECIPE_ERRORS } from '../constants/errorMessages';
@ -41,9 +42,21 @@ const RecipeList: React.FC = () => {
const { showErrorMessage } = useMessage(); const { showErrorMessage } = useMessage();
const EMPTY_RECIPEWITHID = {
recipeName: '',// レシピ名
summary: '',// レシピ概要
recipeId: 0, // レシピID
maxServings: 0 // 最大調理可能数
}
// すべての料理リスト // すべての料理リスト
const [allRecipes, setAllRecipes] = useState<RecipeWithId[]>(); const [allRecipes, setAllRecipes] = useState<RecipeWithId[]>();
const [openAddByRecipeDialog, setOpenAddByRecipeDialog] = useState(false);
const [addByRecipeId, setAddByRecipeId] = useState(0);
const [numOfPeople, setNumOfPeople] = useState('1');
const [checked, setChecked] = useState(false);
const openRecipeById = (recipeId: number) => { const openRecipeById = (recipeId: number) => {
navigate('/addRecipe/' + recipeId); navigate('/addRecipe/' + recipeId);
} }
@ -51,10 +64,12 @@ const RecipeList: React.FC = () => {
const fetchAllRecipes = async () => { const fetchAllRecipes = async () => {
try { try {
const recipes = await recipeApi.getAllRecipes(); const recipes = await recipeApi.getAllRecipes();
console.log('All recipes:');
console.log(recipes);
setAllRecipes(recipes); setAllRecipes(recipes);
} catch (error) { } catch (error) {
console.error(`${RECIPE_ERRORS.FETCH_FAILED}:`, error);
showErrorMessage(RECIPE_ERRORS.FETCH_FAILED); showErrorMessage(RECIPE_ERRORS.FETCH_FAILED);
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
} }
}; };
@ -64,45 +79,93 @@ const RecipeList: React.FC = () => {
}, []); }, []);
return ( return (
<div className="mainContainer"> <>
<div className="mainTitle"> <div className="mainContainer">
<SoupKitchenIcon sx={{ mr: "0.5em" }} /> <div className="mainTitle">
<SoupKitchenIcon sx={{ mr: "0.5em" }} />
</div>
<div className="listWrapper"> </div>
<List> <div className="listWrapper">
{/* 料理一覧をマップして各タスクをリストアイテムとして表示 */} <List>
{!allRecipes {/* 料理一覧をマップして各タスクをリストアイテムとして表示 */}
? <p>...</p> {!allRecipes
: allRecipes.map((recipe, index) => ( ? <p>...</p>
<ListItem : allRecipes.map((recipe, index) => (
key={recipe.recipeId} <ListItem
sx={{ key={recipe.recipeId}
color: 'primary', sx={{
bgcolor: 'background.paper', color: 'primary',
mb: 1, bgcolor: 'background.paper',
borderRadius: 1, mb: 1,
boxShadow: 1, borderRadius: 1,
fontSize: '1.5rem' boxShadow: 1,
}} fontSize: '1.5rem'
onClick={() => openRecipeById(recipe.recipeId)} }}
> onClick={() => {
{recipe.recipeName} setAddByRecipeId(recipe.recipeId)
</ListItem> setOpenAddByRecipeDialog(true)
))} }
</List> }
>
<ListItemText
primary={recipe.recipeName}
/>
<ListItemSecondaryAction>
{recipe.maxServings ?
<Typography variant="body1" component="span" sx={{ marginRight: '1em', color: "mediumaquamarine" }}>
{recipe.maxServings}
</Typography>
: <></>}
{recipe.maxServings === 0 ?
<Tooltip title="" sx={{ color: "tomato" }}>
<IconButton edge="end" aria-label="">
<CloseIcon />
</IconButton>
</Tooltip> :
<Tooltip title="" sx={{ color: "mediumaquamarine" }}>
<IconButton edge="end" aria-label="">
<TaskAltIcon />
</IconButton>
</Tooltip>
}
<Tooltip title="編集">
<IconButton edge="end" aria-label="編集"
onClick={() => {
navigate('/addRecipe/' + recipe.recipeId);
}}
>
<EditIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</div>
</div> </div>
{/* 買うものリストへ追加ダイアログ */}
{
<AddByRecipeDialog openDialog={openAddByRecipeDialog} setOpenDialog={setOpenAddByRecipeDialog}
recipeId = {addByRecipeId} numOfPeople={numOfPeople} setNumOfPeaple={setNumOfPeople}
checked = {checked} setChecked={setChecked}
/>
}
{/* 追加ボタン - 画面下部に固定表示 */}
<Box className="plusButtonWrapper"> <Box className="plusButtonWrapper">
<Fab color="primary" onClick={() => navigate('/addRecipe')} className="plusButton"> <Fab color="primary" onClick={() => navigate('/addRecipe/')} className="plusButton">
<AddIcon /> <AddIcon />
</Fab> </Fab>
{/* <Typography className="plusButtonLabel"> {/* <Typography className="plusButtonLabel">
</Typography> */} </Typography> */}
</Box> </Box>
</>
</div>
); );
}; };

@ -31,16 +31,19 @@ import { GENERAL_ERRORS, STOCK_ERRORS } from '../constants/errorMessages';
import { Add as AddIcon, KeyboardArrowDown as ArrowDownIcon, KeyboardArrowUp as ArrowUpIcon, Inventory as InventoryIcon } from '@mui/icons-material'; import { Add as AddIcon, KeyboardArrowDown as ArrowDownIcon, KeyboardArrowUp as ArrowUpIcon, Inventory as InventoryIcon } from '@mui/icons-material';
import DatePicker, { registerLocale } from 'react-datepicker'; import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート
/*import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート*/
import { useMessage } from '../components/MessageContext'; import { useMessage } from '../components/MessageContext';
import BuyExpDateSelect from '../components/BuyExpDateSelect';
import { STOCK_MESSAGES } from '../constants/normalMessages';
// 日付をyyyy-MM-dd形式で返す関数 /*// yyyy-MM-dd形式で返す関数
const formatDateLocal = (date: Date) => { const formatDateLocal = (date: Date) => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };*/
// 新規在庫の初期状態 // 新規在庫の初期状態
const EMPTY_STOCK: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null } & { newAddition: boolean } = { const EMPTY_STOCK: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null } & { newAddition: boolean } = {
@ -59,7 +62,7 @@ const EMPTY_STOCK: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null
} }
// 日本語ロケールを登録 // 日本語ロケールを登録
registerLocale('ja', ja); //registerLocale('ja', ja);
const StockPage: React.FC = () => { const StockPage: React.FC = () => {
@ -78,7 +81,7 @@ const StockPage: React.FC = () => {
// 在庫の編集状態 // 在庫の編集状態
const [editStock, setEditStock] = useState<Stock | null>(null); const [editStock, setEditStock] = useState<Stock | null>(null);
const { showErrorMessage, showWarningMessage } = useMessage(); const { showErrorMessage, showWarningMessage, showSuccessMessage } = useMessage();
// カテゴリ名一覧 // カテゴリ名一覧
const CATEGORY_NAMES = [ const CATEGORY_NAMES = [
@ -121,8 +124,21 @@ const StockPage: React.FC = () => {
if (newStock.newAddition) { if (newStock.newAddition) {
newStock.stuffId = null; newStock.stuffId = null;
} }
if (isNaN(newStock.amount)) return;
if (isNaN(newStock.price)) return; if (!newStock.stuffId && !newStock.stuffName) {
showErrorMessage(GENERAL_ERRORS.INVALID_STUFF_NAME);
return;
}
if (isNaN(newStock.amount)) {
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return;
}
if (newStock.price === null || isNaN(newStock.price)) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return;
}
if (newStock.buyAmount !== null && isNaN(newStock.buyAmount)) { if (newStock.buyAmount !== null && isNaN(newStock.buyAmount)) {
newStock.buyAmount = null; newStock.buyAmount = null;
@ -132,23 +148,25 @@ const StockPage: React.FC = () => {
console.log(newStock) console.log(newStock)
// 購入日と消費・賞味期限の整合性チェック // 購入日と消費・賞味期限の整合性チェック
const buy = new Date(newStock.buyDate); if (newStock.expDate !== null) {
const exp = new Date(newStock.expDate); const buy = new Date(newStock.buyDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得 const exp = new Date(newStock.expDate);
const buyDateOnly = new Date(buy); // 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
buyDateOnly.setHours(0, 0, 0, 0); const buyDateOnly = new Date(buy);
const expDateOnly = new Date(exp); buyDateOnly.setHours(0, 0, 0, 0);
expDateOnly.setHours(0, 0, 0, 0); const expDateOnly = new Date(exp);
expDateOnly.setHours(0, 0, 0, 0);
// console.log("新規作成buy:", buy);
// console.log("新規作成exp:", exp); // console.log("新規作成buy:", buy);
// console.log("新規作成buyDateOnly:", buyDateOnly); // console.log("新規作成exp:", exp);
// console.log("新規作成expDateOnly:", expDateOnly); // console.log("新規作成buyDateOnly:", buyDateOnly);
// console.log("新規作成expDateOnly:", expDateOnly);
if (buyDateOnly.getTime() > expDateOnly.getTime()) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。"); if (buyDateOnly.getTime() > expDateOnly.getTime()) {
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.'); // alert("購入日は消費・賞味期限より前の日付を設定してください。");
return; showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
} }
const today = new Date().toISOString().substring(0, 10); const today = new Date().toISOString().substring(0, 10);
@ -157,6 +175,7 @@ const StockPage: React.FC = () => {
console.log("送信するデータ:", updatedStock); // 送信前のデータを確認 console.log("送信するデータ:", updatedStock); // 送信前のデータを確認
await stockApi.addStock(updatedStock); // 修正したオブジェクトを API に送信 await stockApi.addStock(updatedStock); // 修正したオブジェクトを API に送信
showSuccessMessage(STOCK_MESSAGES.CREATE_OK);
// await stockApi.addStock(newStock); // await stockApi.addStock(newStock);
setIsAddOpen(false); // ダイアログを閉じる setIsAddOpen(false); // ダイアログを閉じる
@ -175,10 +194,12 @@ const StockPage: React.FC = () => {
try { try {
await stockApi.updateStock(request); await stockApi.updateStock(request);
fetchStocks(); // 削除後の買うもの一覧を再取得 fetchStocks(); // 削除後の買うもの一覧を再取得
// showSuccessMessage(STOCK_MESSAGES.UPDATE_OK);
} catch (error) { } catch (error) {
console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error); console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.UPDATE_FAILED); // showErrorMessage(STOCK_ERRORS.UPDATE_FAILED);
} }
}; };
/** /**
@ -188,6 +209,9 @@ const StockPage: React.FC = () => {
const handleDeleteStock = async (stockId: number) => { const handleDeleteStock = async (stockId: number) => {
try { try {
await stockApi.deleteStock(stockId); await stockApi.deleteStock(stockId);
showSuccessMessage(STOCK_MESSAGES.DELETE_OK);
fetchStocks(); // 削除後の買うもの一覧を再取得 fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) { } catch (error) {
console.error(`${STOCK_ERRORS.DELETE_FAILED}:`, error); console.error(`${STOCK_ERRORS.DELETE_FAILED}:`, error);
@ -237,23 +261,25 @@ const StockPage: React.FC = () => {
if (!editStock) return; if (!editStock) return;
const { stockId, amount, buyAmount, price, shop, buyDate, expDate, lastUpdate } = editStock; const { stockId, amount, buyAmount, price, shop, buyDate, expDate, lastUpdate } = editStock;
// 購入日が消費・賞味期限より未来の場合はエラー表示 if (expDate !== null) {
const buy = new Date(buyDate); // 購入日が消費・賞味期限より未来の場合はエラー表示
const exp = new Date(expDate); const buy = new Date(buyDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得 const exp = new Date(expDate);
const buyDateOnly = new Date(buy); // 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
buyDateOnly.setHours(0, 0, 0, 0); const buyDateOnly = new Date(buy);
const expDateOnly = new Date(exp); buyDateOnly.setHours(0, 0, 0, 0);
expDateOnly.setHours(0, 0, 0, 0); const expDateOnly = new Date(exp);
// console.log("編集buy:", buy); expDateOnly.setHours(0, 0, 0, 0);
// console.log("編集exp:", exp); // console.log("編集buy:", buy);
// console.log("編集buyDateOnly:", buyDateOnly); // console.log("編集exp:", exp);
// console.log("編集expDateOnly:", expDateOnly); // console.log("編集buyDateOnly:", buyDateOnly);
// console.log("編集expDateOnly:", expDateOnly);
if (buy > exp) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。"); if (buy > exp) {
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.'); // alert("購入日は消費・賞味期限より前の日付を設定してください。");
return; showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
} }
try { try {
@ -269,14 +295,14 @@ const StockPage: React.FC = () => {
setIsDeleteOpen(true); // 削除ダイアログを開く setIsDeleteOpen(true); // 削除ダイアログを開く
return; return;
} }
if (!numericAmount || !numericBuyAmount) { if (!numericAmount /* || !numericBuyAmount */) {
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT); showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return; return;
} }
if (!numericPrice) { // if (!numericPrice) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE); // showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return; // return;
} // }
const lastUpdate = new Date().toISOString().substring(0, 10); const lastUpdate = new Date().toISOString().substring(0, 10);
@ -295,6 +321,9 @@ const StockPage: React.FC = () => {
await handleUpdateStock(updateRequest); await handleUpdateStock(updateRequest);
console.log(STOCK_MESSAGES.UPDATE_OK)
showSuccessMessage(STOCK_MESSAGES.UPDATE_OK);
setSelectedRow(editStock); // 更新後に選択行を反映 setSelectedRow(editStock); // 更新後に選択行を反映
fetchStocks(); // 最新データを取得 fetchStocks(); // 最新データを取得
setSelectedRow(null); // 選択解除 setSelectedRow(null); // 選択解除
@ -373,21 +402,25 @@ const StockPage: React.FC = () => {
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredStocks.map(stock => { {filteredStocks.map(stock => {
const today = new Date(); let daysLeft = null;
const expDate = new Date(stock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得 if (stock.expDate !== null) {
const todayDateOnly = new Date(today); const today = new Date();
todayDateOnly.setHours(0, 0, 0, 0); const expDate = new Date(stock.expDate);
const expDateOnly = new Date(expDate); // 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
expDateOnly.setHours(0, 0, 0, 0); const todayDateOnly = new Date(today);
const timeDiff = expDateOnly.getTime() - todayDateOnly.getTime(); todayDateOnly.setHours(0, 0, 0, 0);
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24)); const expDateOnly = new Date(expDate);
expDateOnly.setHours(0, 0, 0, 0);
// console.log("テーブルtoday:", today); const timeDiff = expDateOnly.getTime() - todayDateOnly.getTime();
// console.log("テーブルexp:", expDate); daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
// console.log("テーブルtodayDateOnly:", todayDateOnly);
// console.log("テーブルexpDateOnly:", expDateOnly); // console.log("テーブルtoday:", today);
// console.log("日数差:", daysLeft); // console.log("テーブルexp:", expDate);
// console.log("テーブルtodayDateOnly:", todayDateOnly);
// console.log("テーブルexpDateOnly:", expDateOnly);
// console.log("日数差:", daysLeft);
}
return ( return (
<TableRow <TableRow
@ -398,9 +431,9 @@ const StockPage: React.FC = () => {
<TableCell align="center" sx={{ width: '40%', fontSize: '16px' }}>{stock.stuffName}</TableCell> <TableCell align="center" sx={{ width: '40%', fontSize: '16px' }}>{stock.stuffName}</TableCell>
<TableCell align="center" sx={{ width: '20%', fontSize: '16px' }}>{stock.amount}</TableCell> <TableCell align="center" sx={{ width: '20%', fontSize: '16px' }}>{stock.amount}</TableCell>
<TableCell align="center" sx={{ width: '40%', fontSize: '16px' }} <TableCell align="center" sx={{ width: '40%', fontSize: '16px' }}
style={daysLeft <= 3 ? { color: "red", fontWeight: "bold" } : {}} style={daysLeft !== null && daysLeft <= 3 ? { color: "red", fontWeight: "bold" } : {}}
> >
{formatDate(stock.expDate)} {stock.expDate && formatDate(stock.expDate)}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
@ -485,53 +518,9 @@ const StockPage: React.FC = () => {
onChange={handleChange} onChange={handleChange}
/> />
{/* 購入日・消費期限を横並びに */} {/* 購入日・賞味期限入力 */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}> <BuyExpDateSelect newStock={editStock} setNewStock={({ buyDate, expDate }) => setEditStock({ ...editStock, buyDate, expDate })} />
{/* 購入日フィールド */}
<DatePicker
selected={editStock.buyDate ? new Date(editStock.buyDate) : null}
onChange={(date) => {
if (editStock) {
setEditStock({
...editStock,
buyDate: date ? formatDateLocal(date) : '',
});
}
}}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="normal"
label="購入日 (yyyy/MM/dd)"
fullWidth
name="buyDate"
/>
}
isClearable
/>
{/* 消費・賞味期限フィールド */}
<DatePicker
selected={editStock.expDate ? new Date(editStock.expDate) : null}
onChange={(date) => {
if (editStock) {
setEditStock({
...editStock,
expDate: date ? formatDateLocal(date) : '',
});
}
}}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="normal"
label="消費・賞味期限 (yyyy/MM/dd)"
fullWidth
name="expDate"
/>
}
isClearable
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3, mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3, mb: 2 }}>
<Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }}> <Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }}>
@ -718,46 +707,14 @@ const StockPage: React.FC = () => {
label="購入店舗" label="購入店舗"
fullWidth fullWidth
value={newStock.shop} value={newStock.shop}
onChange={(e) => setNewStock({...newStock, shop: e.target.value})} onChange={(e) => setNewStock({ ...newStock, shop: e.target.value })}
/> />
{/* 購入日・消費期限を横並びに */} {/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}> <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
<DatePicker {/* 購入日・賞味期限入力 */}
popperClassName="custom-datepicker-popper" <BuyExpDateSelect newStock={newStock} setNewStock={({ buyDate, expDate }) => setNewStock({ ...newStock, buyDate, expDate })} />
selected={newStock.buyDate ? new Date(newStock.buyDate) : null}
onChange={(date) =>
setNewStock({ ...newStock, buyDate: date ? formatDateLocal(date) : '' })
}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="dense"
label="購入日(yyyy/MM/dd)"
fullWidth
/>
}
isClearable
//withPortal // ← 他の文字との重なり対策
/>
{/* 消費・賞味期限入力フィールド */}
<DatePicker
popperClassName="custom-datepicker-popper"
selected={newStock.expDate ? new Date(newStock.expDate) : null}
onChange={(date) =>
setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' })
}
dateFormat="yyyy/MM/dd"
customInput={
<TextField
margin="dense"
label="消費・賞味期限(yyyy/MM/dd)"
fullWidth
/>
}
isClearable
//withPortal
/>
</Box> </Box>
</Box> </Box>
</DialogContent> </DialogContent>
@ -772,15 +729,15 @@ const StockPage: React.FC = () => {
{/* 各カテゴリを表示 */} {/* 各カテゴリを表示 */}
{CATEGORY_NAMES.map(category => { {CATEGORY_NAMES.map(category => {
return ( return (
<Box sx={{ padding: "1rem" }}> <Box sx={{ padding: "1rem" }}>
<Typography variant="h5" component="h1" gutterBottom <Typography variant="h5" component="h1" gutterBottom
onClick={() => setOpenCategory({...openCategory, [category]: !openCategory[category]})} onClick={() => setOpenCategory({ ...openCategory, [category]: !openCategory[category] })}
> >
{!openCategory[category] ? <ArrowDownIcon color="primary" /> : <ArrowUpIcon color="primary" />} {!openCategory[category] ? <ArrowDownIcon color="primary" /> : <ArrowUpIcon color="primary" />}
{category} {category}
</Typography> </Typography>
{openCategory[category] && StockTable(stocks, [category])} {openCategory[category] && StockTable(stocks, [category])}
</Box> </Box>
) )
})} })}
@ -793,7 +750,7 @@ const StockPage: React.FC = () => {
</Typography> */} </Typography> */}
</Box> </Box>
</div> </div>
); );
}; };

@ -33,6 +33,7 @@ import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
import BuyDialog from '../components/BuyDialog'; import BuyDialog from '../components/BuyDialog';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import MessageAlert from '../components/MessageAlert';
import { useMessage } from '../components/MessageContext'; import { useMessage } from '../components/MessageContext';
import StuffHistoryDialog from '../components/StuffHistoryDialog'; import StuffHistoryDialog from '../components/StuffHistoryDialog';
@ -71,8 +72,8 @@ const TaskListPage: React.FC = () => {
amount: '', // 購入数量(ここではstring) amount: '', // 購入数量(ここではstring)
price: '', // ここではstring price: '', // ここではstring
shop: '', shop: '',
buyDate: new Date().toISOString(), buyDate: '', // 日付選択側で今日の日付がデフォルトで選択されている
expDate: '', expDate: '', // 未設定状態
}); });
//削除確認ダイアログの表示状態 //削除確認ダイアログの表示状態
@ -116,6 +117,7 @@ const TaskListPage: React.FC = () => {
setToBuys(tobuys); setToBuys(tobuys);
} catch (error) { } catch (error) {
console.error(`${TOBUY_ERRORS.FETCH_FAILED}:`, error); console.error(`${TOBUY_ERRORS.FETCH_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.FETCH_FAILED);
} }
}; };
@ -145,6 +147,7 @@ const TaskListPage: React.FC = () => {
fetchTasks(); // 削除後の買うもの一覧を再取得 fetchTasks(); // 削除後の買うもの一覧を再取得
} catch (error) { } catch (error) {
console.error(`${TOBUY_ERRORS.DELETE_FAILED}:`, error); console.error(`${TOBUY_ERRORS.DELETE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.DELETE_FAILED);
} }
}; };
@ -170,12 +173,14 @@ const TaskListPage: React.FC = () => {
console.log(newToBuy) console.log(newToBuy)
await toBuyApi.addToBuy(newToBuy); await toBuyApi.addToBuy(newToBuy);
setOpenAddToBuyDialog(false); // ダイアログを閉じる setOpenAddToBuyDialog(false); // ダイアログを閉じる
// alert(TOBUY_MESSAGES.CREATE_OK);
showSuccessMessage(TOBUY_MESSAGES.CREATE_OK); showSuccessMessage(TOBUY_MESSAGES.CREATE_OK);
setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
fetchTasks(); // 作成後のタスク一覧を再取得 fetchTasks(); // 作成後のタスク一覧を再取得
} catch (error) { } catch (error) {
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error); console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.CREATE_FAILED);
} }
}; };
@ -198,6 +203,7 @@ const TaskListPage: React.FC = () => {
fetchTasks(); // 作成後のタスク一覧を再取得 fetchTasks(); // 作成後のタスク一覧を再取得
} catch (error) { } catch (error) {
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error); console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.CREATE_FAILED);
} }
}; };
@ -206,20 +212,32 @@ const TaskListPage: React.FC = () => {
*/ */
const handleBuy = async () => { const handleBuy = async () => {
try { try {
// 今日の日付文字列(日付部分のみ)
const today = new Date().toISOString().substring(0, 10); const today = new Date().toISOString().substring(0, 10);
const parsedPrice = parseInt(newStock.price, 10); const parsedPrice = parseInt(newStock.price, 10);
console.log("newPrice:", newStock.price); console.log("newPrice:", newStock.price);
console.log("parsedPrice: ", parsedPrice); console.log("parsedPrice: ", parsedPrice);
// 購入価格のバリデーション
if (isNaN(parsedPrice)) { if (isNaN(parsedPrice)) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE); showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return; return;
//setNewStock({ ...newStock, price: parsedPrice });
} }
// 購入数量のバリデーション
const amount = parseInt(newStock.amount, 10); const amount = parseInt(newStock.amount, 10);
if (isNaN(amount)) { if (isNaN(amount)) {
showErrorMessage(TOBUY_ERRORS.INVALID_BUYAMOUNT); showErrorMessage(TOBUY_ERRORS.INVALID_BUYAMOUNT);
return; return;
} }
// 購入日のバリデーション(未設定の場合今日を設定)
if (!newStock.buyDate) {
newStock.buyDate = today;
}
await toBuyApi.buy({ await toBuyApi.buy({
tobuyId: selectedToBuyId, tobuyId: selectedToBuyId,
...newStock, ...newStock,
@ -227,10 +245,14 @@ const TaskListPage: React.FC = () => {
price: parsedPrice, price: parsedPrice,
lastUpdate: today lastUpdate: today
}); //データベースに送信 }); //データベースに送信
setOpenInfoDialog(false); setOpenInfoDialog(false);
fetchTasks(); // 変更後後の買うもの一覧を再取得 fetchTasks(); // 変更後後の買うもの一覧を再取得
} catch (error) { } catch (error) {
console.error(`${TOBUY_ERRORS.BUY_FAILED}:`, error); console.error(`${TOBUY_ERRORS.BUY_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.BUY_FAILED);
} }
}; };

@ -3,7 +3,7 @@
* APIとの通信を担当するモジュール * APIとの通信を担当するモジュール
* *
*/ */
import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest } from '../types/types'; import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest, RecipeDetailWithId } from '../types/types';
import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages'; import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用 // APIのベースURL - 環境変数から取得するか、デフォルト値を使用
@ -188,12 +188,20 @@ export const toBuyApi = {
/** /**
* *
*/ */
buy: async (req: { tobuyId: number, amount: number, price: number, shop: string, expDate: string, buyDate: string, lastUpdate: string }): Promise<{ result: boolean }> => { buy: async (req: {
tobuyId: number,
amount: number,
price: number,
shop: string,
buyDate: string,
expDate: string | null,
lastUpdate: string
}): Promise<{ result: boolean }> => {
console.log('/api/tobuy/buy request: ', req) console.log('/api/tobuy/buy request: ', req)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' req.buyDate = parseDate(req.buyDate) || getToday();
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || '' req.expDate = parseDate(req.expDate)
const response = await fetch(`${API_BASE_URL}/api/tobuy/buy`, { const response = await fetch(`${API_BASE_URL}/api/tobuy/buy`, {
method: 'POST', method: 'POST',
@ -286,8 +294,8 @@ export const stockApi = {
console.log("送信するデータ:", stock); // 送信前のデータ確認 console.log("送信するデータ:", stock); // 送信前のデータ確認
stock.buyDate = makeDateObject(stock.buyDate)?.toISOString()?.substring(0, 10) || '' stock.buyDate = parseDate(stock.buyDate) || getToday();
stock.expDate = makeDateObject(stock.expDate)?.toISOString()?.substring(0, 10) || '' stock.expDate = parseDate(stock.expDate)
console.log("変換後のデータ:", stock); // 日付変換後のデータ確認 console.log("変換後のデータ:", stock); // 日付変換後のデータ確認
@ -317,8 +325,8 @@ export const stockApi = {
updateStock: async (req: StockUpdateRequest): Promise<{ result: boolean; message: string }> => { updateStock: async (req: StockUpdateRequest): Promise<{ result: boolean; message: string }> => {
// console.log('req: ', req) // console.log('req: ', req)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' req.buyDate = parseDate(req.buyDate) || getToday();
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || '' req.expDate = parseDate(req.expDate)
console.log('req: ', req) console.log('req: ', req)
@ -469,12 +477,7 @@ export const recipeApi = {
* @param recipeId ID * @param recipeId ID
* @returns * @returns
*/ */
getById: async (recipeId: number): Promise<{ getById: async (recipeId: number): Promise<RecipeDetailWithId> => {
recipeId: number;
recipeName: string;
summary: string;
stuffAndAmountArray: StuffAndCategoryAndAmount[];
}> => {
const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, { const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, {
method: 'GET', method: 'GET',
headers: getHeaders(), headers: getHeaders(),
@ -490,15 +493,22 @@ export const recipeApi = {
}; };
function makeDateObject(dateStr: String) { function parseDate(dateStr: string | null) {
// 例: '2025/06/15' または '2025-06-15' を '2025-06-15' に変換 // 例: '2025/06/15' または '2025-06-15' を '2025-06-15' に変換
const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割 if (dateStr) {
if (parts.length === 3) { const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割
return new Date(parts[0] + '-' + parts[1] + '-' + parts[2]); if (parts.length === 3) {
const date = new Date(parts[0] + '-' + parts[1] + '-' + parts[2]);
return date.toISOString().substring(0, 10);
}
} }
return null; // 無効な日付の場合 return null; // 無効な日付の場合
} }
function getToday() {
return new Date().toISOString().substring(0, 10);
}
// /** // /**
// * (サンプル,実際には不要) // * (サンプル,実際には不要)

@ -62,10 +62,10 @@ export interface StockUpdateRequest {
stockId: number, stockId: number,
amount: number, amount: number,
buyAmount: number | null, buyAmount: number | null,
price: number, price: number | null,
shop: string | null, shop: string | null,
buyDate: string, buyDate: string,
expDate: string, expDate: string | null,
lastUpdate: string, lastUpdate: string,
} }
@ -97,7 +97,7 @@ export interface NewStock {
price: string, price: string,
shop: string, shop: string,
buyDate: string, buyDate: string,
expDate: string, expDate: string | null,
} }
/** /**
@ -160,4 +160,21 @@ export interface RecipeDetail extends Recipe {
category?: string; // 新規材料カテゴリ(オプション) category?: string; // 新規材料カテゴリ(オプション)
amount: number; // 使用量(必須) amount: number; // 使用量(必須)
}[]; }[];
}
/**
*
* (ID, 調)
*/
export interface RecipeDetailWithId extends RecipeWithId {
// 材料リスト(直接配列として内包)
stuffAndAmountArray: {
// 既存材料IDまたは新規作成情報のどちらか一方のみ必要
stuffId: number; // 材料ID
stuffName: string; // 材料名
category: string; // 材料カテゴリ
amount: number; // 数量
}[];
} }
Loading…
Cancel
Save