Merge branch 'feature-frontend-alert' into develop-frontend

feature-frontend-recipe-api
Masaharu.Kato 9 months ago
commit 3559705d5d
  1. 20
      backend/src/main/resources/application-localfwd.yml
  2. 15
      frontend/src/components/BuyDialog.tsx
  3. 69
      frontend/src/components/Layout.tsx
  4. 29
      frontend/src/components/MessageAlert.tsx
  5. 17
      frontend/src/components/MessageContext.tsx
  6. 33
      frontend/src/pages/AddRecipe.tsx
  7. 7
      frontend/src/pages/DishList.tsx
  8. 9
      frontend/src/pages/RecipeList.tsx
  9. 107
      frontend/src/pages/StockPage.tsx
  10. 12
      frontend/src/pages/TaskListPage.tsx

@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/${LOCAL_DB_NAME}
driver-class-name: org.postgresql.Driver
username: ${LOCAL_DB_USER}
password: ${LOCAL_DB_PASSWORD}
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
server:
address: 0.0.0.0
port: 8080
cors:
allowed-origins: http://${WIN_IP}:3000

@ -26,12 +26,14 @@ const formatDateLocal = (date: Date) => {
const BuyDialog = ({
openDialog,
setOpenDialog,
stuffName,
newStock,
setNewStock,
onSubmit,
}: {
openDialog: boolean,
setOpenDialog: (open: boolean) => void,
stuffName: string,
newStock: NewStock,
setNewStock: (tobuy: NewStock) => void,
onSubmit: () => void,
@ -40,11 +42,21 @@ const BuyDialog = ({
return (
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '500px', maxHeight: '80vh' } }}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }}
>
<DialogTitle></DialogTitle>
<DialogContent>
<Box sx={{ pt: 1 }}>
{/* 材料名表示 */}
<TextField
margin="dense"
label="材料名"
fullWidth
value={stuffName}
disabled
sx={{ marginBottom: 2 , marginTop: 2}}
/>
{/* 価格入力フィールド */}
<TextField
autoFocus
@ -58,6 +70,7 @@ const BuyDialog = ({
setNewStock({ ...newStock, price: value })
};
}}
sx={{ marginBottom: 2 }}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>

@ -2,7 +2,7 @@
*
* AppBar
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
AppBar,
Toolbar,
@ -17,6 +17,7 @@ import {
ListItemButton,
Divider,
IconButton,
AlertColor,
BottomNavigation,
BottomNavigationAction,
Paper
@ -30,6 +31,8 @@ import {
ShoppingCart as ShoppingCartIcon
} from '@mui/icons-material';
import { useNavigate, Outlet, useLocation } from 'react-router-dom';
import { MessageContext } from './MessageContext';
import MessageAlert from './MessageAlert';
const Layout: React.FC = () => {
const navigate = useNavigate();
@ -55,6 +58,55 @@ const Layout: React.FC = () => {
setDrawerOpen(!drawerOpen);
};
// メッセージ表示
// ページ遷移後もメッセージを維持
useEffect(() => {
const saved = sessionStorage.getItem('globalMessage');
if (saved) {
const { message, severity } = JSON.parse(saved);
showMessage(message, severity);
}
}, []);
const [msgOpen, setMsgOpen] = useState(false);
const [msgText, setMsgText] = useState('');
const [msgType, setMsgType] = useState<AlertColor>('info');
const showMessage = (msg: string, sev: AlertColor) => {
setMsgText(msg);
setMsgType(sev);
setMsgOpen(true);
sessionStorage.setItem('globalMessage', JSON.stringify({ message: msg, severity: sev }));
};
const showErrorMessage = (message: string) => showMessage(message, 'error');
const showWarningMessage = (message: string) => showMessage(message, 'warning');
const showInfoMessage = (message: string) => showMessage(message, 'info');
const showSuccessMessage = (message: string) => showMessage(message, 'success');
const handleMsgClose = () => {
setMsgOpen(false);
// setMsgText(''); // ここで空にすると,メッセージが消えるアニメーションが始まる時点で文字が消えてしまう
sessionStorage.removeItem('globalMessage');
};
const handleBottomNavigation = (event: React.SyntheticEvent, newValue: any) => {
setBottomNavi(newValue);
switch(newValue) {
case 0:
navigate('stock');
break;
case 1:
navigate('tasks');
break;
case 2:
navigate('recipeList');
break;
}
// ここでルーティング処理などを行う
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
{/* ヘッダー部分 - アプリ名とログアウトボタンを表示 */}
@ -69,10 +121,6 @@ const Layout: React.FC = () => {
</Toolbar>
</AppBar>
<Paper sx={{ position: 'fixed', bottom: 0, left: 0, right: 0 }} elevation={3}>
<BottomNavigation
showLabels
@ -99,11 +147,20 @@ const Layout: React.FC = () => {
</BottomNavigation>
</Paper>
{/* メインコンテンツ領域 - 子ルートのコンポーネントがここに表示される */}
<Box component="main" sx={{ flexGrow: 1, bgcolor: 'background.default', py: 3 }}>
<Container>
<MessageContext.Provider value={{ showErrorMessage, showWarningMessage, showSuccessMessage, showInfoMessage }}>
<MessageAlert
open={msgOpen}
message={msgText}
severity={msgType}
onClose={handleMsgClose}
/>
<Outlet /> {/* React Router の Outlet - 子ルートのコンポーネントがここにレンダリングされる */}
</MessageContext.Provider>
</Container>
</Box>
</Box>

@ -0,0 +1,29 @@
import React from 'react';
import { Snackbar, Alert, AlertColor } from '@mui/material';
interface MessageAlertProps {
open: boolean;
message: string;
severity: AlertColor; // 'error' | 'warning' | 'info' | 'success'
onClose: (event?: React.SyntheticEvent | Event, reason?: string) => void;
duration?: number;
}
const MessageAlert: React.FC<MessageAlertProps> = ({
open,
message,
severity,
onClose,
duration = 6000,
}) => {
return (
<Snackbar open={open} autoHideDuration={duration} onClose={onClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} sx={{bottom: '120px'}}>
<Alert onClose={onClose} severity={severity} sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>
);
};
export default MessageAlert;

@ -0,0 +1,17 @@
import React, { createContext, useContext } from 'react';
export interface MessageContextType {
showErrorMessage: (message: string) => void;
showWarningMessage: (message: string) => void;
showSuccessMessage: (message: string) => void;
showInfoMessage: (message: string) => void;
}
export const MessageContext = createContext<MessageContextType | undefined>(undefined);
export const useMessage = () => {
const context = useContext(MessageContext);
if (!context) throw new Error('useMessage must be used within MessageContext.Provider');
return context;
};

@ -25,7 +25,8 @@ import {
Select,
FormControl,
InputLabel,
ListItemIcon
ListItemIcon,
AlertColor
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
@ -36,6 +37,8 @@ import { StuffAndCategoryAndAmount } from '../types/types';
import EditAmountDialog from '../components/EditAmountDialog';
import { recipeApi, toBuyApi } from '../services/api';
import { useNavigate, useParams } from 'react-router-dom';
import MessageAlert from '../components/MessageAlert';
import { useMessage } from '../components/MessageContext';
const AddRecipe: React.FC = () => {
const { recipeId: recipeIdStr } = useParams();
@ -43,6 +46,8 @@ const AddRecipe: React.FC = () => {
const navigate = useNavigate();
// 編集時,既存情報を読み込んだかどうか
const [recipeLoaded, setRecipeLoaded] = useState(false);
// 料理名,説明
const [recipeName, setRecipeName] = useState<string>('');
const [recipeSummary, setRecipeSummary] = useState<string>('');
@ -61,28 +66,33 @@ const AddRecipe: React.FC = () => {
//削除確認ダイアログの表示状態
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
// エラーメッセージ表示
const { showErrorMessage, showSuccessMessage } = useMessage();
const loadRecipe = async () => {
if (recipeId && !recipeName) {
if (recipeId && !recipeLoaded) {
const recipe = await recipeApi.getById(recipeId);
console.log('loaded recipe=', recipe)
setRecipeName(recipe.recipeName)
setRecipeSummary(recipe.summary)
setItems(recipe.stuffAndAmountArray)
setRecipeLoaded(true)
}
}
const handleSaveRecipe = async () => {
if (!recipeName) {
alert('レシピ名が入力されていません!')
showErrorMessage('レシピ名が入力されていません!')
return false;
}
if (!items.length) {
alert('材料が追加されていません!')
showErrorMessage('材料が追加されていません!')
return false;
}
try {
if (!recipeId) {
// 新規追加
const response = await recipeApi.addRecipe({
@ -99,14 +109,18 @@ const AddRecipe: React.FC = () => {
summary: recipeSummary,
stuffAndAmountArray: items,
})
} catch {
showErrorMessage('レシピの送信に失敗しました。同じ料理名が存在する可能性があります。');
return false;
}
return recipeId;
}
const handleSubmit = async () => {
const recipeId = await handleSaveRecipe();
// alert('レシピが保存されました!');
if (!recipeId) return;
showSuccessMessage('レシピが保存されました!');
navigate('/recipeList');
}
@ -114,7 +128,7 @@ const AddRecipe: React.FC = () => {
const recipeId = await handleSaveRecipe();
if (!recipeId) return false;
await toBuyApi.addByRecipe(recipeId);
// alert('レシピが保存されて買うものリストに追加されました!');
showSuccessMessage('レシピが保存されて買うものリストに追加されました!');
navigate('/tasks');
}
@ -124,7 +138,8 @@ const AddRecipe: React.FC = () => {
}, []);
return (
(recipeId && !recipeName)
<>
{(recipeId && !recipeLoaded)
? <p>...</p>
:
<Box>
@ -206,7 +221,7 @@ const AddRecipe: React.FC = () => {
</Fab>
</div>
<div style={{ position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "2%", 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" }}>
<SaveIcon sx={{ fontSize: "1.5rem", marginRight: "0.5rem" }} />
@ -258,6 +273,8 @@ const AddRecipe: React.FC = () => {
}} />
</Box>
}
</>
);
};

@ -159,9 +159,10 @@ const DishList: React.FC = () => {
))}
{/* </List> */}
</div>
<div style={{width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "20px"}}>
<Button variant='contained' sx={{width: "60%", height: "60px",
fontSize: "40px", left: "50%", transform: 'translateX(-50%)' }}
<div style={{width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px"}}>
<Button variant='contained' sx={{
width: "60%",
fontSize: "24px", left: "50%", transform: 'translateX(-50%)' }}
color="primary"
onClick={() => navigate('/add1')}
>

@ -31,11 +31,14 @@ import {
SoupKitchen as SoupKitchenIcon
} from '@mui/icons-material';
import { ToBuy, Stuff, RecipeWithId, RecipeDetail } from '../types/types';
import { useMessage } from '../components/MessageContext';
const RecipeList: React.FC = () => {
const navigate = useNavigate();
// 料理リストの料理名を格納する配列
const { showErrorMessage } = useMessage();
// すべての料理リスト
const [allRecipes, setAllRecipes] = useState<RecipeWithId[]>();
@ -48,7 +51,7 @@ const RecipeList: React.FC = () => {
const recipes = await recipeApi.getAllRecipes();
setAllRecipes(recipes);
} catch (error) {
alert("レシピの取得に失敗しました.");
showErrorMessage("レシピの取得に失敗しました。");
// console.error(`${TASK_ERRORS.FETCH_FAILED}:`, error);
}
};
@ -93,10 +96,10 @@ const RecipeList: React.FC = () => {
))}
{/* </List> */}
</div>
<div style={{ width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "20px" }}>
<div style={{ width: "100%", position: "fixed", left: "50%", transform: 'translateX(-50%)', bottom: "64px" }}>
<Button variant='contained' sx={{
width: "60%", height: "60px",
fontSize: "40px", left: "50%", transform: 'translateX(-50%)'
fontSize: "32px", left: "50%", transform: 'translateX(-50%)'
}}
color="primary"
onClick={() => navigate('/AddRecipe')}

@ -29,6 +29,7 @@ import {
import { STOCK_ERRORS } from '../constants/errorMessages';
import DatePicker, { registerLocale } from 'react-datepicker';
import { ja } from 'date-fns/locale/ja'; // date-fnsの日本語ロケールをインポート
import { useMessage } from '../components/MessageContext';
// 日付をyyyy-MM-dd形式で返す関数
const formatDateLocal = (date: Date) => {
@ -72,6 +73,8 @@ const StockPage: React.FC = () => {
// 在庫の編集状態
const [editStock, setEditStock] = useState<Stock | null>(null);
const { showWarningMessage } = useMessage();
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
fetchStocks();
@ -146,7 +149,7 @@ const StockPage: React.FC = () => {
};
/**
*
*
*/
const onChangeCategory = async (category: string) => {
setNewStock({ ...newStock, category })
@ -196,7 +199,7 @@ const StockPage: React.FC = () => {
setEditStock({ ...selectedRow });
setIsEditOpen(true);
} else {
alert("編集する食材を選択してください。");
showWarningMessage("編集する食材を選択してください。");
}
};
// 変更を適用
@ -248,7 +251,7 @@ const StockPage: React.FC = () => {
if (selectedRow) {
setIsDeleteOpen(true);
} else {
alert("削除する食材を選択してください。");
showWarningMessage("削除する食材を選択してください。");
}
};
/** 削除ダイアログを閉じる */
@ -270,8 +273,6 @@ const StockPage: React.FC = () => {
<TableRow>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
@ -290,8 +291,6 @@ const StockPage: React.FC = () => {
>
<TableCell>{stock.stuffName}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell>{stock.price}</TableCell>
<TableCell>{formatDate(stock.buyDate)}</TableCell>
<TableCell
style={daysLeft <= 3 ? { color: "red", fontWeight: "bold" } : {}}
>
@ -330,6 +329,54 @@ const StockPage: React.FC = () => {
value={editStock.price}
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>
{/*
<TextField
label="購入日 (yyyy-MM-dd)"
fullWidth
@ -345,18 +392,15 @@ const StockPage: React.FC = () => {
name="expDate"
value={editStock.expDate}
onChange={handleChange}
/>
<Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }} sx={{ mt: 3, mb: 2, left: '68%' }}></Button>
<Button
variant="contained"
color="success"
onClick={handleApplyChanges}
sx={{ mt: 3, mb: 2, left: "68%" }}
>
/> */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3, mb: 2 }}>
<Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }}>
</Button>
<Button variant="contained" color="success" onClick={handleApplyChanges}>
</Button>
</Box>
</>
)}
</DialogContent>
@ -375,13 +419,26 @@ const StockPage: React.FC = () => {
<>
<Typography variant="h4">{selectedRow.stuffName}</Typography>
<Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography>
<Button onClick={() => { setIsDeleteOpen(false); setSelectedRow(null); }} sx={{ mt: 3, mb: 2, left: '70%' }}></Button>
<Button variant="contained" color="error" onClick={() => {
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 3, mb: 2 }}>
<Button onClick={() => {
setIsDeleteOpen(false);
setSelectedRow(null);
}}>
</Button>
<Button
variant="contained"
color="error"
onClick={() => {
handleDeleteStock(selectedRow.stockId);
setIsDeleteOpen(false); // 削除処理後にダイアログを閉じる
setSelectedRow(null); // セルの選択を解除
setIsDeleteOpen(false);
setSelectedRow(null);
}}
style={{ marginTop: "10px" }} sx={{ mt: 3, mb: 2, left: '72%' }}></Button>
>
</Button>
</Box>
</>
)}
</DialogContent>
@ -398,6 +455,7 @@ const StockPage: React.FC = () => {
</Typography>
<Box sx={{ textAlign: 'right' }}>
{/* <Box sx={{ position: 'fixed', top: 16, right: 16, zIndex: 1000, display: 'flex', gap: 2 }}> */}
{/* 在庫の食材追加ボタン */}
<Button variant="contained" color="primary" onClick={handleOpenAdd} sx={{ mt: 3, mb: 2, mr: 1 }}>
@ -554,8 +612,9 @@ const StockPage: React.FC = () => {
</Dialog>
{/* 在庫の食材編集ボタン(全テーブル共通) */}
<Button variant="contained" color="success" onClick={handleOpenEdit} sx={{ mt: 3, mb: 2, mr: 1 }}>
<Button variant="contained" color="success" onClick={handleOpenEdit} sx={{
mt: 3, mb: 2, mr: 1 }}>
</Button>
{/* 在庫の食材削除ボタン (全テーブル共通) */}

@ -31,6 +31,7 @@ import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
import BuyDialog from '../components/BuyDialog';
import { useNavigate } from 'react-router-dom';
import DatePicker from 'react-datepicker';
import { useMessage } from '../components/MessageContext';
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
@ -91,7 +92,7 @@ const TaskListPage: React.FC = () => {
shop: undefined,
});
const { showErrorMessage } = useMessage();
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
@ -149,7 +150,7 @@ const TaskListPage: React.FC = () => {
const handleAddNewToBuy = async () => {
try {
if (isNaN(newToBuy.amount)) {
console.log('数量が正しくありません.');
showErrorMessage('数量が正しくありません.');
return;
}
@ -171,7 +172,7 @@ const TaskListPage: React.FC = () => {
const handleUpdateNewToBuy = async () => {
try {
if (isNaN(editingItem.amount)) {
console.log('数量が正しくありません.');
showErrorMessage('数量が正しくありません.');
return;
}
@ -195,7 +196,7 @@ const TaskListPage: React.FC = () => {
console.log("newPrice:", newStock.price)
console.log("parsedPrice: ", parsedPrice)
if (isNaN(parsedPrice)) {
alert('入力が無効です')
showErrorMessage('価格が正しく入力されていません。')
return
//setNewStock({ ...newStock, price: parsedPrice });
}
@ -251,6 +252,7 @@ const TaskListPage: React.FC = () => {
<IconButton color="primary" sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="購入情報を記入"
onClick={() => {
setOpenInfoDialog(true)
setEditingItem(tobuy)
setSelectedToBuyId(tobuy.tobuyId)
// handleDeleteTask(tobuy.tobuyId)
}}>
@ -330,7 +332,7 @@ const TaskListPage: React.FC = () => {
<AddStuffAmountDialog openDialog={openAddToBuyDialog} setOpenDialog={setOpenAddToBuyDialog} newItem={newToBuy} setNewItem={setNewToBuy} onSubmit={handleAddNewToBuy} />
{/* 購入処理(在庫登録)のための数値入力ダイアログ */}
<BuyDialog openDialog={openInfoDialog} setOpenDialog={setOpenInfoDialog} newStock={newStock} setNewStock={setNewStock} onSubmit={handleBuy} />
<BuyDialog openDialog={openInfoDialog} setOpenDialog={setOpenInfoDialog} stuffName={editingItem.stuffName} newStock={newStock} setNewStock={setNewStock} onSubmit={handleBuy} />
{/* 数量変更ダイアログ */}
<EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog}

Loading…
Cancel
Save