Compare commits

...

8 Commits

  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. 69
      frontend/src/components/BuyDialog.tsx
  6. 80
      frontend/src/components/BuyExpDateSelect.tsx
  7. 10
      frontend/src/constants/errorMessages.ts
  8. 16
      frontend/src/constants/normalMessages.ts
  9. 38
      frontend/src/pages/AddRecipe.tsx
  10. 3
      frontend/src/pages/RecipeList.tsx
  11. 261
      frontend/src/pages/StockPage.tsx
  12. 54
      frontend/src/pages/TaskListPage.tsx
  13. 46
      frontend/src/services/api.ts
  14. 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;

@ -9,6 +9,7 @@ import {
Box,
} from '@mui/material';
import { NewStock } from '../types/types';
import BuyExpDateSelect from './BuyExpDateSelect';
/*import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート*/
@ -89,73 +90,9 @@ const BuyDialog = ({
sx={{ marginBottom: 2 }}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
<TextField
margin="dense"
label="購入日"
type="date"
fullWidth
value={newStock.buyDate ? newStock.buyDate.substring(0, 10) : ''}
onChange={(e) =>
setNewStock({ ...newStock, buyDate: e.target.value })
}
InputLabelProps={{ shrink: true }}
/>
{/*<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 // ← 他の文字との重なり対策
/>*/}
{/* 購入日・賞味期限入力 */}
<BuyExpDateSelect newStock={newStock} setNewStock={({buyDate, expDate}) => setNewStock({...newStock, buyDate, expDate}) } />
{/* 消費・賞味期限入力フィールド */}
<TextField
margin="dense"
label="消費・賞味期限"
type="date"
fullWidth
value={newStock.expDate ? newStock.expDate.substring(0, 10) : ''}
onChange={(e) =>
setNewStock({ ...newStock, expDate: e.target.value })
}
InputLabelProps={{ shrink: true }}
InputProps={{
inputProps: {
min: newStock.buyDate ? newStock.buyDate.substring(0, 10) : undefined,
}
}}
/>
{/*<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"

@ -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;

@ -6,6 +6,7 @@
// 一般的なエラーメッセージ
export const GENERAL_ERRORS = {
UNEXPECTED_ERROR: '予期せぬエラーが発生しました',
INVALID_STUFF_NAME: '材料名が正しく入力されていません。',
INVALID_AMOUNT: '数量が正しく入力されていません。',
INVALID_PRICE: '価格が正しく入力されていません。',
};
@ -23,6 +24,7 @@ export const TOBUY_ERRORS = {
UPDATE_FAILED: '買うものリストの更新に失敗しました',
DELETE_FAILED: '買うものリストの削除に失敗しました',
BUY_FAILED: '買うものリストの購入処理に失敗しました',
INVALID_BUYAMOUNT: '購入数量が正しく入力されていません。',
};
// 材料リスト関連のエラーメッセージ(料理の追加編集で用いる)
@ -40,7 +42,6 @@ export const STOCK_ERRORS = {
CREATE_FAILED: '在庫リストの作成に失敗しました',
UPDATE_FAILED: '在庫リストの更新に失敗しました',
DELETE_FAILED: '在庫リストの削除に失敗しました',
BUY_FAILED: '在庫リストの購入処理に失敗しました',
};
// 料理リスト関連のエラーメッセージ
@ -50,8 +51,15 @@ export const RECIPE_ERRORS = {
CREATE_FAILED: '料理リストの作成に失敗しました',
UPDATE_FAILED: '料理リストの更新に失敗しました',
DELETE_FAILED: '料理リストの削除に失敗しました',
INVALID_RECIPE_NAME: 'レシピ名が入力されていません!',
NO_STUFFS: '材料が追加されていません!',
INVALID_SERVINGS: '人数が入力されていません。',
};
// 購入履歴関連のエラーメッセージ
export const STUFF_HISTORY_ERRORS = {
FETCH_FAILED: '履歴の取得に失敗しました。',
};
// // タスク関連のエラーメッセージ
// export const TASK_ERRORS = {

@ -0,0 +1,16 @@
/**
*
* 使
*/
// 買うものリスト
export const TOBUY_MESSAGES = {
CREATE_OK: '買うものリストに追加されました!',
};
// 料理リスト
export const RECIPE_MESSAGES = {
SAVED: 'レシピが保存されました!',
SAVED_AND_ADDED: 'レシピが保存されて買うものリストに追加されました!',
NO_ADDITIONALS: '必要な食材が在庫にあったのでリストには追加されませんでした。',
}

@ -33,6 +33,8 @@ 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();
@ -66,7 +68,7 @@ const AddRecipe: React.FC = () => {
const [checked, setChecked] = useState(false);
// エラーメッセージ表示
const { showErrorMessage, showSuccessMessage } = useMessage();
const { showErrorMessage, showInfoMessage, showSuccessMessage } = useMessage();
const loadRecipe = async () => {
if (recipeId && !recipeLoaded) {
@ -83,19 +85,19 @@ const AddRecipe: React.FC = () => {
if (!recipeName) {
showErrorMessage('レシピ名が入力されていません!')
console.log("yes1");
// console.log("yes1");
return false;
}
if (!items.length) {
showErrorMessage('材料が追加されていません!')
console.log("yes2");
// console.log("yes2");
return false;
}
try {
if (!recipeId) {
console.log("yes3");
// console.log("yes3");
// 新規追加
const response = await recipeApi.addRecipe({
recipeName,
@ -115,21 +117,21 @@ const AddRecipe: React.FC = () => {
showErrorMessage('レシピの送信に失敗しました。同じ料理名が存在する可能性があります。');
return false;
}
console.log("yes4");
// console.log("yes4");
return recipeId;
}
const checkRecipeAndItems = async () => {
if (!recipeName) {
showErrorMessage('レシピ名が入力されていません!')
console.log("yes1");
showErrorMessage(RECIPE_ERRORS.INVALID_RECIPE_NAME);
// console.log("yes1");
return false;
}
if (!items.length) {
showErrorMessage('材料が追加されていません!')
console.log("yes2");
showErrorMessage(RECIPE_ERRORS.NO_STUFFS);
// console.log("yes2");
return false;
}
return true;
@ -138,20 +140,20 @@ const AddRecipe: React.FC = () => {
const handleSubmit = async () => {
const recipeId = await handleSaveRecipe();
if (!recipeId) return;
showSuccessMessage('レシピが保存されました!');
showSuccessMessage(RECIPE_MESSAGES.SAVED);
navigate('/recipeList');
}
const handleSubmitAndAddToBuy = async () => {
console.log("too");
console.log(recipeName);
// console.log("too");
console.log('recipeName:', recipeName);
const recipeId = await handleSaveRecipe();
console.log("before");
// console.log("before");
if (!recipeId) return false;
console.log("ds");
// console.log("ds");
if (!numOfPeaple) {
showErrorMessage('人数が入力されていません!')
console.log("yes2");
showErrorMessage(RECIPE_ERRORS.INVALID_SERVINGS);
// console.log("yes2");
return false;
}
const finalAddResult = await toBuyApi.addByRecipe(recipeId, numOfPeaple, checked);
@ -175,10 +177,10 @@ const AddRecipe: React.FC = () => {
// }))
// });
if (finalAddResult.data.length === 0) {
showSuccessMessage('必要な食材が在庫にあったのでリストには追加されませんでした!');
showInfoMessage(RECIPE_MESSAGES.NO_ADDITIONALS);
}
else {
showSuccessMessage('レシピが保存されて買うものリストに追加されました!');
showSuccessMessage(RECIPE_MESSAGES.SAVED_AND_ADDED);
}
navigate('/tasks');
}

@ -32,6 +32,7 @@ import {
} from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import { useMessage } from '../components/MessageContext';
import { RECIPE_ERRORS } from '../constants/errorMessages';
const RecipeList: React.FC = () => {
const navigate = useNavigate();
@ -51,7 +52,7 @@ const RecipeList: React.FC = () => {
const recipes = await recipeApi.getAllRecipes();
setAllRecipes(recipes);
} catch (error) {
showErrorMessage("レシピの取得に失敗しました。");
showErrorMessage(RECIPE_ERRORS.FETCH_FAILED);
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
}
};

@ -31,6 +31,7 @@ import { Add as AddIcon } from '@mui/icons-material';
/*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';
/*// yyyy-MM-dd形式で返す関数
const formatDateLocal = (date: Date) => {
@ -107,7 +108,7 @@ const StockPage: React.FC = () => {
newStock.stuffId = null;
}
if (isNaN(newStock.amount)) return;
if (isNaN(newStock.price)) return;
if (newStock.price === null || isNaN(newStock.price)) return;
if (newStock.buyAmount !== null && isNaN(newStock.buyAmount)) {
newStock.buyAmount = null;
@ -117,23 +118,25 @@ const StockPage: React.FC = () => {
console.log(newStock)
// 購入日と消費・賞味期限の整合性チェック
const buy = new Date(newStock.buyDate);
const exp = new Date(newStock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const buyDateOnly = new Date(buy);
buyDateOnly.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("新規作成buyDateOnly:", buyDateOnly);
// console.log("新規作成expDateOnly:", expDateOnly);
if (buyDateOnly.getTime() > expDateOnly.getTime()) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。");
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
if (newStock.expDate !== null) {
const buy = new Date(newStock.buyDate);
const exp = new Date(newStock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const buyDateOnly = new Date(buy);
buyDateOnly.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("新規作成buyDateOnly:", buyDateOnly);
// console.log("新規作成expDateOnly:", expDateOnly);
if (buyDateOnly.getTime() > expDateOnly.getTime()) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。");
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
}
const today = new Date().toISOString().substring(0, 10);
@ -231,23 +234,25 @@ const StockPage: React.FC = () => {
if (!editStock) return;
const { stockId, amount, buyAmount, price, shop, buyDate, expDate, lastUpdate } = editStock;
// 購入日が消費・賞味期限より未来の場合はエラー表示
const buy = new Date(buyDate);
const exp = new Date(expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const buyDateOnly = new Date(buy);
buyDateOnly.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("編集buyDateOnly:", buyDateOnly);
// console.log("編集expDateOnly:", expDateOnly);
if (buy > exp) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。");
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
if (expDate !== null) {
// 購入日が消費・賞味期限より未来の場合はエラー表示
const buy = new Date(buyDate);
const exp = new Date(expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const buyDateOnly = new Date(buy);
buyDateOnly.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("編集buyDateOnly:", buyDateOnly);
// console.log("編集expDateOnly:", expDateOnly);
if (buy > exp) {
// alert("購入日は消費・賞味期限より前の日付を設定してください。");
showErrorMessage('購入日は消費・賞味期限より前の日付を設定してください.');
return;
}
}
try {
@ -367,21 +372,25 @@ const StockPage: React.FC = () => {
</TableHead>
<TableBody>
{filteredStocks.map(stock => {
const today = new Date();
const expDate = new Date(stock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const todayDateOnly = new Date(today);
todayDateOnly.setHours(0, 0, 0, 0);
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));
// console.log("テーブルtoday:", today);
// console.log("テーブルexp:", expDate);
// console.log("テーブルtodayDateOnly:", todayDateOnly);
// console.log("テーブルexpDateOnly:", expDateOnly);
// console.log("日数差:", daysLeft);
let daysLeft = null;
if (stock.expDate !== null) {
const today = new Date();
const expDate = new Date(stock.expDate);
// 時間をリセットして純粋な“日付のみ”のタイムスタンプを取得
const todayDateOnly = new Date(today);
todayDateOnly.setHours(0, 0, 0, 0);
const expDateOnly = new Date(expDate);
expDateOnly.setHours(0, 0, 0, 0);
const timeDiff = expDateOnly.getTime() - todayDateOnly.getTime();
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
@ -392,9 +401,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>
);
@ -479,79 +488,9 @@ const StockPage: React.FC = () => {
onChange={handleChange}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日 */}
<TextField
margin="normal"
label="購入日"
type="date"
fullWidth
name="buyDate"
value={editStock.buyDate ? editStock.buyDate.substring(0, 10) : ''}
onChange={(e) => setEditStock({ ...editStock, buyDate: e.target.value })}
InputLabelProps={{ shrink: true }}
/>
{/*<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
/>*/}
{/* 消費・賞味期限 */}
<TextField
margin="normal"
label="消費・賞味期限"
type="date"
fullWidth
name="expDate"
value={editStock.expDate ? editStock.expDate.substring(0, 10) : ''}
onChange={(e) => setEditStock({ ...editStock, expDate: e.target.value })}
InputLabelProps={{ shrink: true }}
InputProps={{
inputProps: {
min: newStock.buyDate ? newStock.buyDate.substring(0, 10) : undefined,
}
}}
/>
{/*<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); }}>
@ -767,74 +706,12 @@ const StockPage: React.FC = () => {
label="購入店舗"
fullWidth
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 }}>
{/* 購入日入力フィールド */}
<TextField
margin="dense"
label="購入日"
type="date"
fullWidth
value={newStock.buyDate ? newStock.buyDate.substring(0, 10) : ''}
onChange={(e) =>
setNewStock({ ...newStock, buyDate: e.target.value })
}
InputLabelProps={{ shrink: true }}
/>
{/*<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="消費・賞味期限"
type="date"
fullWidth
value={newStock.expDate ? newStock.expDate.substring(0, 10) : ''}
onChange={(e) =>
setNewStock({ ...newStock, expDate: e.target.value })
}
InputLabelProps={{ shrink: true }}
InputProps={{
inputProps: {
min: newStock.buyDate ? newStock.buyDate.substring(0, 10) : undefined,
}
}}
/>
{/*<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 })} />
</Box>
</DialogContent>
<DialogActions>

@ -25,7 +25,8 @@ import {
SoupKitchen as SoupKitchenIcon, Edit as EditIcon
} from '@mui/icons-material';
import { ToBuy, Stuff, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, StockHistory, /*Stock*/ } from '../types/types';
import { GENERAL_ERRORS, TOBUY_ERRORS } from '../constants/errorMessages';
import { GENERAL_ERRORS, TOBUY_ERRORS, STUFF_HISTORY_ERRORS } from '../constants/errorMessages';
import { TOBUY_MESSAGES } from '../constants/normalMessages';
import EditAmountDialog from '../components/EditAmountDialog';
import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
import BuyDialog from '../components/BuyDialog';
@ -69,8 +70,8 @@ const TaskListPage: React.FC = () => {
amount: '', // 購入数量(ここではstring)
price: '', // ここではstring
shop: '',
buyDate: new Date().toISOString(),
expDate: '',
buyDate: '', // 日付選択側で今日の日付がデフォルトで選択されている
expDate: '', // 未設定状態
});
//削除確認ダイアログの表示状態
@ -96,7 +97,7 @@ const TaskListPage: React.FC = () => {
shop: undefined,
});
const { showErrorMessage } = useMessage();
const { showErrorMessage, showSuccessMessage } = useMessage();
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
@ -114,6 +115,7 @@ const TaskListPage: React.FC = () => {
setToBuys(tobuys);
} catch (error) {
console.error(`${TOBUY_ERRORS.FETCH_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.FETCH_FAILED);
}
};
@ -143,6 +145,7 @@ const TaskListPage: React.FC = () => {
fetchTasks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${TOBUY_ERRORS.DELETE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.DELETE_FAILED);
}
};
@ -158,13 +161,23 @@ const TaskListPage: React.FC = () => {
return;
}
newToBuy.stuffName = newToBuy.stuffName.trim(); // 材料名の前後の空白を削除
if (!newToBuy.stuffName) {
showErrorMessage(GENERAL_ERRORS.INVALID_STUFF_NAME);
return;
}
console.log(newToBuy)
await toBuyApi.addToBuy(newToBuy);
setOpenAddToBuyDialog(false); // ダイアログを閉じる
showSuccessMessage(TOBUY_MESSAGES.CREATE_OK);
setNewToBuy(EMPTY_TOBUY); // 入力内容をリセット
fetchTasks(); // 作成後のタスク一覧を再取得
} catch (error) {
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.CREATE_FAILED);
}
};
@ -187,6 +200,7 @@ const TaskListPage: React.FC = () => {
fetchTasks(); // 作成後のタスク一覧を再取得
} catch (error) {
console.error(`${TOBUY_ERRORS.CREATE_FAILED}:`, error);
showErrorMessage(TOBUY_ERRORS.CREATE_FAILED);
}
};
@ -195,20 +209,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)
console.log("newPrice:", newStock.price);
console.log("parsedPrice: ", parsedPrice);
// 購入価格のバリデーション
if (isNaN(parsedPrice)) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE)
return
//setNewStock({ ...newStock, price: parsedPrice });
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return;
}
// 購入数量のバリデーション
const amount = parseInt(newStock.amount, 10);
if (isNaN(amount)) {
showErrorMessage('購入数量が正しく入力されていません。')
return
showErrorMessage(TOBUY_ERRORS.INVALID_BUYAMOUNT);
return;
}
// 購入日のバリデーション(未設定の場合今日を設定)
if (!newStock.buyDate) {
newStock.buyDate = today;
}
await toBuyApi.buy({
tobuyId: selectedToBuyId,
...newStock,
@ -216,10 +242,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);
}
};
@ -236,7 +266,7 @@ const TaskListPage: React.FC = () => {
setStockHistories(await stockApi.getHistories(tobuy.stuffId));
setOpenHistoryDialog(true);
} catch {
showErrorMessage("履歴の読み込みに失敗しました。");
showErrorMessage(STUFF_HISTORY_ERRORS.FETCH_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' に変換
const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割
if (parts.length === 3) {
return new Date(parts[0] + '-' + parts[1] + '-' + parts[2]);
if (dateStr) {
const parts = dateStr.split(/[-\/]/); // ハイフンかスラッシュで分割
if (parts.length === 3) {
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