You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
joint_exc/frontend/src/pages/StockPage.tsx

749 lines
27 KiB

/**
* テストページコンポーネント
* 白紙の状態で表示されるテスト用のページ
*/
import React, { useState, useEffect } from 'react';
import { stockApi, stuffApi } from '../services/api';
import { Stock, Stuff } from '../types/types';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material";
import {
Container,
Typography,
Tooltip,
Fab,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Button,
Box,
Checkbox,
FormControlLabel,
FormGroup,
FormControl,
InputLabel,
Select,
MenuItem,
} from '@mui/material';
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) => {
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 } = {
stuffId: null,
stuffName: '',
amount: 1,
price: 0,
lastUpdate: '',
buyDate: new Date().toISOString(),
expDate: '',
category: '',
newAddition: false, // 材料を新規作成するか否か
// shop '',
}
// 日本語ロケールを登録
//registerLocale('ja', ja);
const StockPage: React.FC = () => {
const [stocks, setStocks] = useState<Stock[]>([]);
// セル選択の表示状態
const [selectedRow, setSelectedRow] = useState<Stock | null>(null);
// 追加ダイアログボックスの表示状態
const [isAddOpen, setIsAddOpen] = useState(false);
// 在庫追加に使う状態
const [newStock, setNewStock] = useState(EMPTY_STOCK);
const [stuffs, setStuffs] = useState<Stuff[]>([]);
// 編集ダイアロボックスの表示状態
const [isEditOpen, setIsEditOpen] = useState(false);
// 削除メッセージダイアログの表示状態
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
// 在庫の編集状態
const [editStock, setEditStock] = useState<Stock | null>(null);
const { showWarningMessage } = useMessage();
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
fetchStocks();
}, []);
/**
* APIから在庫一覧を取得する関数
* 取得したタスクをstate(tasks)に設定
*/
const fetchStocks = async () => {
try {
const stocks = await stockApi.getStocks();
console.log('Stocks=', stocks)
setStocks(stocks);
} catch (error) {
console.error(`${STOCK_ERRORS.FETCH_FAILED}:`, error);
}
};
/**
* 在庫リストに新規食材を作成するハンドラー
*/
const handleCreateStock = async () => {
try {
if (newStock.newAddition) {
newStock.stuffId = null;
}
if (isNaN(newStock.amount)) return;
if (isNaN(newStock.price)) return;
console.log(newStock)
const today = new Date().toISOString().substring(0, 10);
const updatedStock = { ...newStock, lastUpdate: today }; // lastUpdate に today を設定
console.log("送信するデータ:", updatedStock); // 送信前のデータを確認
await stockApi.addStock(updatedStock); // 修正したオブジェクトを API に送信
// await stockApi.addStock(newStock);
setIsAddOpen(false); // ダイアログを閉じる
setNewStock(EMPTY_STOCK); // 入力内容をリセット
fetchStocks(); // 作成後のタスク一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.CREATE_FAILED}:`, error);
}
};
/**
* 在庫リストを編集するハンドラー
*/
const handleUpdateStock = async (stockId: number, amount: number, price: number, buyDate: string, expDate: string) => {
try {
const today = new Date().toISOString().substring(0, 10);
await stockApi.updateStock({ stockId, amount, price, lastUpdate: today, buyDate, expDate });
fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error);
}
};
/**
* 在庫を削除するハンドラー
* 指定されたIDのタスクをAPIを通じて削除
*/
const handleDeleteStock = async (stockId: number) => {
try {
await stockApi.deleteStock(stockId);
fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.DELETE_FAILED}:`, error);
}
};
/**
* カテゴリー選択
*/
const onChangeCategory = async (category: string) => {
setNewStock({ ...newStock, category })
const result = await stuffApi.getStuffs(category)
setStuffs(result)
}
/**
* 文字列(ISO 8601形式)をyyyy/MM/ddに変換する関数
*/
const formatDate = (isoString: string): string => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月は0始まりなので+1
const day = date.getDate().toString().padStart(2, "0");
return `${year}/${month}/${day}`;
};
/* Date型をyyyy/MM/ddに変換する関数
const formatDate = (date: Date): string => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月は0始まりなので+1
const day = date.getDate().toString().padStart(2, "0");
return `${year}/${month}/${day}`;
};
*/
/** 追加ボタンを押したときにダイアログを開く */
const handleOpenAdd = () => {
setIsAddOpen(true);
};
/** 追加ダイアログを閉じる */
const handleCloseAdd = () => {
setIsAddOpen(false);
};
/**
* セルを選択する関数. 再度クリックで選択解除 → 行選択ではなくチェックボックスにしたため不要
*/
// const handleRowClick = (stock: Stock) => {
// setSelectedRow(prev => (prev?.stockId === stock.stockId ? null : stock));
// };
// チェックボックス切り替え
const handleCheckboxChange = (stock: Stock) => {
setSelectedRow(prev => (prev?.stockId === stock.stockId ? null : stock));
};
/** 編集ボタンを押したときにダイアログを開く */
// ダイアログを開く際に `selectedRow` の値を `editStock` にセット
const handleOpenEdit = () => {
if (selectedRow) {
setEditStock({ ...selectedRow });
setIsEditOpen(true);
} else {
showWarningMessage("編集する食材を選択してください。");
}
};
// 変更を適用. 数量に0を入力したとき、削除ダイアログに飛ぶ機能を追加
const handleApplyChanges = async () => {
if (!editStock) return;
try {
if (Number(editStock.amount) === 0) {
// 数量が 0 の場合は削除処理へ誘導
setIsEditOpen(false); // 編集ダイアログを閉じる
setSelectedRow(editStock); // 削除対象をセット
setIsDeleteOpen(true); // 削除ダイアログを開く
return;
}
await handleUpdateStock(
editStock.stockId,
Number(editStock.amount),
Number(editStock.price),
editStock.buyDate,
editStock.expDate
);
setSelectedRow(editStock); // 更新後に選択行を反映
fetchStocks(); // 最新データを取得
setSelectedRow(null); // 選択解除
} catch (error) {
console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error);
}
setIsEditOpen(false); // 編集ダイアログを閉じる
};
// ダイアログを開く際に `selectedRow` の値を `editStock` にコピー
useEffect(() => {
if (selectedRow) {
setEditStock({ ...selectedRow });
}
}, [selectedRow]); // `selectedRow` が変更されたら `editStock` に反映
// テキストフィールドの変更を検知
// 負の値を入力できないように書き換え
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
// 数値項目に対して負の値をブロック
const numericFields = ['amount', 'price'];
if (numericFields.includes(name) && Number(value) < 0) {
return; // 無視して更新しない
}
if (editStock) {
setEditStock({
...editStock,
[name]: value,
});
}
};
/** 編集ダイアログを閉じる */
const handleCloseEdit = () => {
setIsEditOpen(false);
};
/** 削除ボタンを押したときにダイアログを開く */
const handleOpenDelete = () => {
if (selectedRow) {
setIsDeleteOpen(true);
} else {
showWarningMessage("削除する食材を選択してください。");
}
};
/** 削除ダイアログを閉じる */
const handleCloseDelete = () => {
setIsDeleteOpen(false);
};
/** テーブルを表示する関数 */
const StockTable = (stocks: Stock[], categories: string[]) => {
const filteredStocks = stocks.filter(stock => categories.includes(stock.category));
if (filteredStocks.length === 0) return null;
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
<TableCell padding="checkbox" />
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredStocks.map(stock => {
const isSelected = selectedRow?.stockId === stock.stockId;
const today = new Date();
const expDate = new Date(stock.expDate);
const timeDiff = expDate.getTime() - today.getTime();
const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
return (
<TableRow
key={stock.stockId}
sx={{
backgroundColor: isSelected ? 'yellow' : 'white',
}}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
onChange={() => handleCheckboxChange(stock)}
/>
</TableCell>
<TableCell>{stock.stuffName}</TableCell>
<TableCell>{stock.amount}</TableCell>
<TableCell
style={daysLeft <= 3 ? { color: "red", fontWeight: "bold" } : {}}
>
{formatDate(stock.expDate)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{/* 編集ダイアログ */}
<Dialog open={isEditOpen} onClose={handleCloseEdit} fullWidth maxWidth="sm">
<DialogTitle></DialogTitle>
<DialogContent>
{editStock && (
<>
<Typography variant="h4">{editStock.stuffName}</Typography>
<TextField
label="数量"
fullWidth
margin="normal"
name="amount"
type="number"
value={editStock.amount}
onChange={handleChange}
inputProps={{ min: 0 }}
onKeyDown={(e) => {
if (e.key === '-' || e.key === 'e' || e.key === 'E') {
e.preventDefault();
}
}}
/>
<TextField
label="購入価格"
fullWidth
margin="normal"
name="price"
type="number"
value={editStock.price}
onChange={handleChange}
inputProps={{ min: 0 }}
onKeyDown={(e) => {
if (e.key === '-' || e.key === 'e' || e.key === 'E') {
e.preventDefault();
}
}}
/>
{/* 購入日・消費期限を横並びに */}
<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>
<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>
</Dialog>
{/* 削除ダイアログ */}
<Dialog open={isDeleteOpen}
onClose={handleCloseDelete}
fullWidth
maxWidth="sm"
sx={{ overflow: "hidden" }}
>
<DialogTitle></DialogTitle>
<DialogContent>
{selectedRow && (
<>
<Typography variant="h4">{selectedRow.stuffName}</Typography>
<Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography>
<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);
}}
>
</Button>
</Box>
</>
)}
</DialogContent>
</Dialog>
</>
);
};
return (
<Container>
<Typography variant="h4" component="h1" gutterBottom>
</Typography>
{/* <Box sx={{ textAlign: 'right' }}> */}
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1000,
backgroundColor: '#f5f5f5',
padding: 2,
display: 'flex',
gap: 0.5,
justifyContent: 'flex-end', // ← 右寄せ
borderBottom: 'none', // ← これで線を消す
boxShadow: 'none', // ← 影も消す
}}
>
{/* 在庫の食材追加ボタン */}
<Button variant="contained" color="primary" onClick={handleOpenAdd} sx={{ mt: 3, mb: 2, mr: 1 }}>
</Button>
{/* 新規タスク作成ダイアログ */}
<Dialog open={isAddOpen} onClose={handleCloseAdd} disableScrollLock={true}>
<Box display="flex" alignItems="center" >
<DialogTitle sx={{ flexGrow: 1 }}></DialogTitle>
<FormGroup row>
<FormControlLabel
control={<Checkbox />}
label="食材を新規追加"
checked={newStock.newAddition}
onChange={(e) => setNewStock({ ...newStock, newAddition: (e.target as HTMLInputElement).checked })}
/>
</FormGroup>
</Box>
<DialogContent>
<Box sx={{ pt: 1 }}>
{/*材料カテゴリ選択 */}
<FormControl sx={{ width: "50%", marginBottom: 2 }}>
<InputLabel id="demo-simple-select-label"></InputLabel>
<Select
labelId="demo-simple-select-label"
value={newStock.category}
onChange={(e) => onChangeCategory(e.target.value)}
>
<MenuItem value="乳製品"></MenuItem>
<MenuItem value="魚・肉"></MenuItem>
<MenuItem value="野菜"></MenuItem>
<MenuItem value="調味料">調</MenuItem>
<MenuItem value="その他"></MenuItem>
</Select>
</FormControl>
{!newStock.newAddition && <FormControl sx={{ width: "100%", marginBottom: 2 }}>
<InputLabel id="demo-simple-select-label"></InputLabel>
<Select
labelId="demo-simple-select-label"
value={newStock.stuffId}
onChange={(e) => setNewStock({ ...newStock, stuffId: Number(e.target.value) })}
>
{stuffs.map((stuff) => (
<MenuItem key={stuff.stuffId} value={stuff.stuffId}>
{stuff.stuffName}
</MenuItem>
))}
</Select>
</FormControl>}
{/* タスクタイトル入力フィールド */}
{newStock.newAddition && <TextField
autoFocus
margin="dense"
label="材料名"
fullWidth
value={newStock.stuffName}
onChange={(e) => setNewStock({ ...newStock, stuffName: e.target.value })}
sx={{ marginBottom: 2 }}
/>}
{/* 数量入力フィールド */}
<TextField
margin="dense"
label="数量"
fullWidth
value={newStock.amount}
onChange={(e) => {
const value = e.target.value;
const parsedValue = parseInt(value, 10); // 数値に変換
if (isNaN(parsedValue) || parsedValue >= 1) { // 入力欄をいったん空欄にできるようにする,ただし空欄でない場合は1以上のみOK
setNewStock({ ...newStock, amount: parsedValue }); // number型で保存
}
}}
// sx={{ width: "50%" }}
type="number"
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
/>
{/* 購入価格入力フィールド */}
<TextField
margin="dense"
label="購入価格"
fullWidth
value={newStock.price}
onChange={(e) => {
const value = e.target.value;
const parsedValue = parseInt(value, 10); // 数値に変換
if (isNaN(parsedValue) || parsedValue >= 0) { // 入力欄をいったん空欄にできるようにする,ただし空欄でない場合は0以上のみOK
setNewStock({ ...newStock, price: parsedValue }); // number型で保存
}
}}
// sx={{ width: "50%" }}
type="number"
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
/>
{/* 購入日・消費期限を横並びに */}
<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>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setIsAddOpen(false)}></Button>
<Button onClick={handleCreateStock} variant="contained">
</Button>
</DialogActions>
</Dialog>
{/* 在庫の食材編集ボタン(全テーブル共通) */}
<Button variant="contained" color="success" onClick={handleOpenEdit} sx={{
mt: 3, mb: 2, mr: 1
}}>
</Button>
{/* 在庫の食材削除ボタン (全テーブル共通) */}
<Button variant="contained" color="error" onClick={handleOpenDelete} sx={{ mt: 3, mb: 2 }}></Button>
</Box>
{/* 在庫一覧リスト */}
{/* 乳製品 */}
<Typography variant="h4" component="h1" gutterBottom></Typography>
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{StockTable(stocks, ["乳製品"])}
</div>
{/* 肉・魚 */}
<Typography variant="h4" component="h1" gutterBottom></Typography>
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{StockTable(stocks, ["魚・肉"])}
</div>
{/* 野菜 */}
<Typography variant="h4" component="h1" gutterBottom></Typography>
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{StockTable(stocks, ["野菜"])}
</div>
{/* 調味料 */}
<Typography variant="h4" component="h1" gutterBottom>調</Typography>
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{StockTable(stocks, ["調味料"])}
</div>
{/* その他 */}
<Typography variant="h4" component="h1" gutterBottom></Typography>
<div style={{ border: '3px solid black', borderRadius: '8px', backgroundColor: '#add8e6', height: 'auto', padding: '20px', marginBottom: "20px" }}>
{StockTable(stocks, ["その他"])}
</div>
</Container>
);
};
export default StockPage;