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. 62
      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. 81
      frontend/src/pages/RecipeList.tsx
  13. 155
      frontend/src/pages/StockPage.tsx
  14. 28
      frontend/src/pages/TaskListPage.tsx
  15. 42
      frontend/src/services/api.ts
  16. 23
      frontend/src/types/types.ts

@ -99,8 +99,12 @@ public class RecipesController {
public ResponseEntity<RecipeDetailDTO> getRecipeById(
Authentication authentication,
@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 {
response.put("result", false);
response.put("message", "編集に失敗しました");
}
return ResponseEntity.ok(response);
}

@ -31,6 +31,12 @@ public class RecipeDetailDTO {
*/
private String summary;
/**
* 最大調理可能数
* そのレシピを何人分調理可能か
*/
private int maxServings;
/**
* 食材リスト
* このレシピに必要な食材とその数量のリスト

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

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

@ -9,19 +9,20 @@ import {
Box,
} from '@mui/material';
import { NewStock } from '../types/types';
import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート
import BuyExpDateSelect from './BuyExpDateSelect';
/*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 year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
};*/
// 日本語ロケールを登録
registerLocale('ja', ja);
//registerLocale('ja', ja);
const BuyDialog = ({
openDialog,
@ -89,54 +90,9 @@ const BuyDialog = ({
sx={{ marginBottom: 2 }}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
<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>
{/* 購入日・賞味期限入力 */}
<BuyExpDateSelect newStock={newStock} setNewStock={({buyDate, expDate}) => setNewStock({...newStock, buyDate, expDate}) } />
{/* 購入店舗入力フィールド */}
<TextField
margin="dense"

@ -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 showMessage = (msg: string, sev: AlertColor) => {
console.log('ShowMessage:', msg, sev);
setMsgText(msg);
setMsgType(sev);
setMsgOpen(true);

@ -32,7 +32,7 @@ const StuffHistoryDialog = ({
}) => {
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' }}>
: {stuffName}
<IconButton
@ -68,7 +68,7 @@ const StuffHistoryDialog = ({
{/* 各ヘッダーセルに white-space: nowrap; を適用 */}
<TableCell sx={{ whiteSpace: 'nowrap' }}></TableCell>
{/* 「購入店舗」ヘッダーも 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>
</TableRow>

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

@ -193,9 +193,13 @@ const AddRecipe: React.FC = () => {
}
const cancelNumOfPeopleDialog = async () => {
const recipeId = await handleSaveRecipe();
if (!recipeId) return false;
setOpenNumOfPeapleDialog(false);
// const allRecipes = await recipeApi.getAllRecipes();
// const allRecipesId = allRecipes.map(recipe => recipe.recipeId);
// 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>)}
<div style={{ position: "fixed", left: "80%", transform: 'translateX(-50%)', bottom: "10%" }}>
<Box sx={{ textAlign: 'center' }}>
{/* <Box sx={{ textAlign: 'center' }}>
<Typography variant="caption"></Typography>
</Box>
</Box> */}
<Fab color="primary" onClick={() => setOpenAddDialog(true)}>
<AddIcon sx={{ fontSize: "1.5rem" }} />
</Fab>
@ -292,11 +294,7 @@ const AddRecipe: React.FC = () => {
<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>
@ -340,49 +338,6 @@ const AddRecipe: React.FC = () => {
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>
</>
}

@ -28,10 +28,11 @@ import {
} from '@mui/material';
import '../App.css';
import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon
Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon, Close as CloseIcon, TaskAlt as TaskAltIcon
} from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import AddByRecipeDialog from '../components/AddByRecipeDialog';
import { useMessage } from '../components/MessageContext';
import { RECIPE_ERRORS } from '../constants/errorMessages';
@ -41,9 +42,21 @@ const RecipeList: React.FC = () => {
const { showErrorMessage } = useMessage();
const EMPTY_RECIPEWITHID = {
recipeName: '',// レシピ名
summary: '',// レシピ概要
recipeId: 0, // レシピID
maxServings: 0 // 最大調理可能数
}
// すべての料理リスト
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) => {
navigate('/addRecipe/' + recipeId);
}
@ -51,10 +64,12 @@ const RecipeList: React.FC = () => {
const fetchAllRecipes = async () => {
try {
const recipes = await recipeApi.getAllRecipes();
console.log('All recipes:');
console.log(recipes);
setAllRecipes(recipes);
} catch (error) {
console.error(`${RECIPE_ERRORS.FETCH_FAILED}:`, error);
showErrorMessage(RECIPE_ERRORS.FETCH_FAILED);
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
}
};
@ -64,6 +79,7 @@ const RecipeList: React.FC = () => {
}, []);
return (
<>
<div className="mainContainer">
<div className="mainTitle">
<SoupKitchenIcon sx={{ mr: "0.5em" }} />
@ -85,24 +101,71 @@ const RecipeList: React.FC = () => {
boxShadow: 1,
fontSize: '1.5rem'
}}
onClick={() => openRecipeById(recipe.recipeId)}
onClick={() => {
setAddByRecipeId(recipe.recipeId)
setOpenAddByRecipeDialog(true)
}
}
>
{recipe.recipeName}
<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>
{/* 買うものリストへ追加ダイアログ */}
{
<AddByRecipeDialog openDialog={openAddByRecipeDialog} setOpenDialog={setOpenAddByRecipeDialog}
recipeId = {addByRecipeId} numOfPeople={numOfPeople} setNumOfPeaple={setNumOfPeople}
checked = {checked} setChecked={setChecked}
/>
}
{/* 追加ボタン - 画面下部に固定表示 */}
<Box className="plusButtonWrapper">
<Fab color="primary" onClick={() => navigate('/addRecipe')} className="plusButton">
<Fab color="primary" onClick={() => navigate('/addRecipe/')} className="plusButton">
<AddIcon />
</Fab>
{/* <Typography className="plusButtonLabel">
</Typography> */}
</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 DatePicker, { registerLocale } from 'react-datepicker';
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 BuyExpDateSelect from '../components/BuyExpDateSelect';
import { STOCK_MESSAGES } from '../constants/normalMessages';
// 日付をyyyy-MM-dd形式で返す関数
/*// yyyy-MM-dd形式で返す関数
const formatDateLocal = (date: Date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
};*/
// 新規在庫の初期状態
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 = () => {
@ -78,7 +81,7 @@ const StockPage: React.FC = () => {
// 在庫の編集状態
const [editStock, setEditStock] = useState<Stock | null>(null);
const { showErrorMessage, showWarningMessage } = useMessage();
const { showErrorMessage, showWarningMessage, showSuccessMessage } = useMessage();
// カテゴリ名一覧
const CATEGORY_NAMES = [
@ -121,8 +124,21 @@ const StockPage: React.FC = () => {
if (newStock.newAddition) {
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)) {
newStock.buyAmount = null;
@ -132,6 +148,7 @@ const StockPage: React.FC = () => {
console.log(newStock)
// 購入日と消費・賞味期限の整合性チェック
if (newStock.expDate !== null) {
const buy = new Date(newStock.buyDate);
const exp = new Date(newStock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
@ -150,6 +167,7 @@ const StockPage: React.FC = () => {
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
}
const today = new Date().toISOString().substring(0, 10);
@ -157,6 +175,7 @@ const StockPage: React.FC = () => {
console.log("送信するデータ:", updatedStock); // 送信前のデータを確認
await stockApi.addStock(updatedStock); // 修正したオブジェクトを API に送信
showSuccessMessage(STOCK_MESSAGES.CREATE_OK);
// await stockApi.addStock(newStock);
setIsAddOpen(false); // ダイアログを閉じる
@ -175,10 +194,12 @@ const StockPage: React.FC = () => {
try {
await stockApi.updateStock(request);
fetchStocks(); // 削除後の買うもの一覧を再取得
// showSuccessMessage(STOCK_MESSAGES.UPDATE_OK);
} catch (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) => {
try {
await stockApi.deleteStock(stockId);
showSuccessMessage(STOCK_MESSAGES.DELETE_OK);
fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.DELETE_FAILED}:`, error);
@ -237,6 +261,7 @@ const StockPage: React.FC = () => {
if (!editStock) return;
const { stockId, amount, buyAmount, price, shop, buyDate, expDate, lastUpdate } = editStock;
if (expDate !== null) {
// 購入日が消費・賞味期限より未来の場合はエラー表示
const buy = new Date(buyDate);
const exp = new Date(expDate);
@ -255,6 +280,7 @@ const StockPage: React.FC = () => {
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
}
try {
// Number型に変換した変数を用意
@ -269,14 +295,14 @@ const StockPage: React.FC = () => {
setIsDeleteOpen(true); // 削除ダイアログを開く
return;
}
if (!numericAmount || !numericBuyAmount) {
if (!numericAmount /* || !numericBuyAmount */) {
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return;
}
if (!numericPrice) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return;
}
// if (!numericPrice) {
// showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
// return;
// }
const lastUpdate = new Date().toISOString().substring(0, 10);
@ -295,6 +321,9 @@ const StockPage: React.FC = () => {
await handleUpdateStock(updateRequest);
console.log(STOCK_MESSAGES.UPDATE_OK)
showSuccessMessage(STOCK_MESSAGES.UPDATE_OK);
setSelectedRow(editStock); // 更新後に選択行を反映
fetchStocks(); // 最新データを取得
setSelectedRow(null); // 選択解除
@ -373,6 +402,9 @@ const StockPage: React.FC = () => {
</TableHead>
<TableBody>
{filteredStocks.map(stock => {
let daysLeft = null;
if (stock.expDate !== null) {
const today = new Date();
const expDate = new Date(stock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
@ -381,13 +413,14 @@ const StockPage: React.FC = () => {
const expDateOnly = new Date(expDate);
expDateOnly.setHours(0, 0, 0, 0);
const timeDiff = expDateOnly.getTime() - todayDateOnly.getTime();
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
// console.log("テーブルtoday:", today);
// console.log("テーブルexp:", expDate);
// console.log("テーブルtodayDateOnly:", todayDateOnly);
// console.log("テーブルexpDateOnly:", expDateOnly);
// console.log("日数差:", daysLeft);
}
return (
<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: '20%', fontSize: '16px' }}>{stock.amount}</TableCell>
<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>
</TableRow>
);
@ -485,53 +518,9 @@ const StockPage: React.FC = () => {
onChange={handleChange}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日フィールド */}
<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>
{/* 購入日・賞味期限入力 */}
<BuyExpDateSelect newStock={editStock} setNewStock={({ buyDate, expDate }) => setEditStock({ ...editStock, buyDate, expDate })} />
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3, mb: 2 }}>
<Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }}>
@ -722,42 +711,10 @@ const StockPage: React.FC = () => {
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
<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 // ← 他の文字との重なり対策
/>
{/* 消費・賞味期限入力フィールド */}
<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
/>
{/* 購入日・賞味期限入力 */}
<BuyExpDateSelect newStock={newStock} setNewStock={({ buyDate, expDate }) => setNewStock({ ...newStock, buyDate, expDate })} />
</Box>
</Box>
</DialogContent>

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

@ -3,7 +3,7 @@
* 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';
// 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)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || ''
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || ''
req.buyDate = parseDate(req.buyDate) || getToday();
req.expDate = parseDate(req.expDate)
const response = await fetch(`${API_BASE_URL}/api/tobuy/buy`, {
method: 'POST',
@ -286,8 +294,8 @@ export const stockApi = {
console.log("送信するデータ:", stock); // 送信前のデータ確認
stock.buyDate = makeDateObject(stock.buyDate)?.toISOString()?.substring(0, 10) || ''
stock.expDate = makeDateObject(stock.expDate)?.toISOString()?.substring(0, 10) || ''
stock.buyDate = parseDate(stock.buyDate) || getToday();
stock.expDate = parseDate(stock.expDate)
console.log("変換後のデータ:", stock); // 日付変換後のデータ確認
@ -317,8 +325,8 @@ export const stockApi = {
updateStock: async (req: StockUpdateRequest): Promise<{ result: boolean; message: string }> => {
// console.log('req: ', req)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || ''
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || ''
req.buyDate = parseDate(req.buyDate) || getToday();
req.expDate = parseDate(req.expDate)
console.log('req: ', req)
@ -469,12 +477,7 @@ export const recipeApi = {
* @param recipeId ID
* @returns
*/
getById: async (recipeId: number): Promise<{
recipeId: number;
recipeName: string;
summary: string;
stuffAndAmountArray: StuffAndCategoryAndAmount[];
}> => {
getById: async (recipeId: number): Promise<RecipeDetailWithId> => {
const response = await fetch(`${API_BASE_URL}/api/recipes/getById?recipeId=${recipeId}`, {
method: 'GET',
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' に変換
if (dateStr) {
const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割
if (parts.length === 3) {
return new Date(parts[0] + '-' + parts[1] + '-' + parts[2]);
const date = new Date(parts[0] + '-' + parts[1] + '-' + parts[2]);
return date.toISOString().substring(0, 10);
}
}
return null; // 無効な日付の場合
}
function getToday() {
return new Date().toISOString().substring(0, 10);
}
// /**
// * (サンプル,実際には不要)

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