Merge remote-tracking branch 'origin/develop-frontend' into feature-frontend-register

feature-frontend-dishedit-kato
akito.nishiwaki 9 months ago
commit 06fa43a174
  1. 3
      backend/src/main/java/com/example/todoapp/controller/StocksController.java
  2. 51
      backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java
  3. 37
      backend/src/main/java/com/example/todoapp/service/StocksService.java
  4. 1585
      frontend/package-lock.json
  5. 13
      frontend/src/pages/AddDishies1.tsx
  6. 263
      frontend/src/pages/StockPage.tsx
  7. 36
      frontend/src/services/api.ts

@ -1,5 +1,6 @@
package com.example.todoapp.controller; package com.example.todoapp.controller;
import com.example.todoapp.dto.AddStocksDTO;
import com.example.todoapp.dto.DeleteStockRequest; import com.example.todoapp.dto.DeleteStockRequest;
import com.example.todoapp.dto.DeleteStockRequestDTO; import com.example.todoapp.dto.DeleteStockRequestDTO;
import com.example.todoapp.dto.StockResponseDTO; import com.example.todoapp.dto.StockResponseDTO;
@ -81,7 +82,7 @@ public class StocksController {
@PostMapping("/add") @PostMapping("/add")
public ResponseEntity<StockDTO> createStock( public ResponseEntity<StockDTO> createStock(
Authentication authentication, Authentication authentication,
@Valid @RequestBody Stocks stock) { @Valid @RequestBody AddStocksDTO stock) {
Stocks createdStock = stockService.createStock(authentication.getName(), stock); Stocks createdStock = stockService.createStock(authentication.getName(), stock);
return ResponseEntity.ok(StockDTO.fromEntity(createdStock)); return ResponseEntity.ok(StockDTO.fromEntity(createdStock));
} }

@ -0,0 +1,51 @@
//--------------------------------
// AddStocksDTO.java
//
//
// 更新履歴:2025/06/12 新規作成
// Copyright(c) 2025 IVIS All rights reserved.
//--------------------------------------------
package com.example.todoapp.dto;
import com.example.todoapp.model.Stocks;
import lombok.Data;
import java.time.LocalDate;
/**
* 在庫のデータ転送オブジェクトDTO
* <p>
* このクラスはクライアントとサーバー間で在庫情報をやり取りするために使用されます
* エンティティとは異なり必要な情報のみを含み関連エンティティへの参照ではなくIDのみを保持します
* </p>
*/
@Data
public class AddStocksDTO {
private Long stuffId;
private String stuffName;
private int amount;
private int price;
private String category;
private LocalDate buyDate;
private LocalDate lastUpdate;
private LocalDate expDate;
/**
* 在庫エンティティからDTOを作成する
*
* @param stock 変換元の在庫エンティティ
* @return 変換されたAddStocksDTOオブジェクト
*/
public static AddStocksDTO fromEntity(Stocks stock) {
AddStocksDTO dto = new AddStocksDTO();
dto.setStuffId(stock.getStuff().getStuffId());
dto.setStuffName(stock.getStuff().getStuffName());
dto.setCategory(stock.getStuff().getCategory());
dto.setAmount(stock.getAmount());
dto.setPrice(stock.getPrice());
dto.setBuyDate(stock.getBuyDate());
dto.setLastUpdate(stock.getLastUpdate());
dto.setExpDate(stock.getExpDate());
return dto;
}
}

@ -1,16 +1,21 @@
package com.example.todoapp.service; package com.example.todoapp.service;
import com.example.todoapp.dto.StockDTO;
import com.example.todoapp.dto.AddStocksDTO;
import com.example.todoapp.dto.UpdateStockRequest; import com.example.todoapp.dto.UpdateStockRequest;
import com.example.todoapp.model.Stocks; import com.example.todoapp.model.Stocks;
import com.example.todoapp.model.Stuffs;
import com.example.todoapp.util.MessageUtils; import com.example.todoapp.util.MessageUtils;
import com.example.todoapp.model.User; import com.example.todoapp.model.User;
import com.example.todoapp.repository.StocksRepository; import com.example.todoapp.repository.StocksRepository;
import com.example.todoapp.repository.StuffsRepository;
import com.example.todoapp.repository.UserRepository; import com.example.todoapp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Stocksテーブルのサービスクラス * Stocksテーブルのサービスクラス
@ -23,6 +28,9 @@ public class StocksService {
@Autowired @Autowired
private StocksRepository stocksRepository; private StocksRepository stocksRepository;
@Autowired
private StuffsRepository stuffsRepository;
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@ -36,10 +44,33 @@ public class StocksService {
* @param stock 追加する在庫情報 * @param stock 追加する在庫情報
* @return 保存された在庫エンティティ * @return 保存された在庫エンティティ
*/ */
public Stocks createStock(String username, Stocks stock) { public Stocks createStock(String username, AddStocksDTO stock) {
User user = getUserByUsername(username); User user = getUserByUsername(username);
stock.setUser(user); Stocks stockEntity = new Stocks();
return stocksRepository.save(stock); Stuffs stuffs;
if (stock.getStuffId() == null) {
// 新しい材料を作成
Stuffs newStuff = new Stuffs();
newStuff.setStuffName(stock.getStuffName());
newStuff.setCategory(stock.getCategory());
stuffs = stuffsRepository.save(newStuff);
} else {
// 材料情報を取得
Optional<Stuffs> existstuffs = stuffsRepository.findById(stock.getStuffId());
if (existstuffs == null) {
throw new RuntimeException("材料がありません");
}
stuffs = existstuffs.get();
}
stockEntity.setStuff(stuffs);
stockEntity.setUser(user);
stockEntity.setAmount(stock.getAmount());
stockEntity.setPrice(stock.getPrice());
stockEntity.setBuyDate(stock.getBuyDate());
stockEntity.setLastUpdate(stock.getLastUpdate());
stockEntity.setExpDate(stock.getExpDate());
return stocksRepository.save(stockEntity);
} }

File diff suppressed because it is too large Load Diff

@ -1,13 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
const AddDishies1: React.FC = () => {
return (
<Box>
{/* 白紙のページ - 何も表示しない */}
</Box>
);
};
export default AddDishies1;

@ -3,8 +3,8 @@
* *
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { stockApi } from '../services/api'; import { stockApi, stuffApi } from '../services/api';
import { Stock } from '../types/types'; import { Stock, Stuff } from '../types/types';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material"; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material";
import { import {
Container, Container,
@ -18,15 +18,40 @@ import {
TextField, TextField,
Button, Button,
Box, Box,
Checkbox,
FormControlLabel,
FormGroup,
FormControl,
InputLabel,
Select,
MenuItem,
} from '@mui/material'; } from '@mui/material';
import { STOCK_ERRORS } from '../constants/errorMessages'; import { STOCK_ERRORS } from '../constants/errorMessages';
// 新規在庫の初期状態
const EMPTY_STOCK: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null } & { newAddition: boolean } = {
stuffId: null,
stuffName: '',
amount: 0,
price: 0,
lastUpdate: '',
buyDate: '',
expDate: '',
category: '',
newAddition: false, // 材料を新規作成するか否か
// shop '',
}
const StockPage: React.FC = () => { const StockPage: React.FC = () => {
const [stocks, setStocks] = useState<Stock[]>([]); const [stocks, setStocks] = useState<Stock[]>([]);
// セル選択の表示状態 // セル選択の表示状態
const [selectedRow, setSelectedRow] = useState<Stock | null>(null); 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 [isEditOpen, setIsEditOpen] = useState(false);
// 削除メッセージダイアログの表示状態 // 削除メッセージダイアログの表示状態
@ -53,6 +78,31 @@ const StockPage: React.FC = () => {
} }
}; };
/**
*
*/
const handleCreateStock = async () => {
try {
if (newStock.newAddition) {
newStock.stuffId = null;
}
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);
}
};
/** /**
* *
*/ */
@ -79,6 +129,15 @@ const StockPage: React.FC = () => {
} }
}; };
/**
*
*/
const onChangeCategory = async (category: string) => {
setNewStock({ ...newStock, category })
const result = await stuffApi.getStuffs(category)
setStuffs(result)
}
/** /**
* (ISO 8601)yyyy/MM/ddに変換する関数 * (ISO 8601)yyyy/MM/ddに変換する関数
*/ */
@ -98,6 +157,15 @@ const StockPage: React.FC = () => {
}; };
*/ */
/** 追加ボタンを押したときにダイアログを開く */
const handleOpenAdd = () => {
setIsAddOpen(true);
};
/** 削除ダイアログを閉じる */
const handleCloseAdd = () => {
setIsAddOpen(false);
};
/** /**
* . * .
*/ */
@ -187,28 +255,40 @@ const StockPage: React.FC = () => {
<TableCell></TableCell> <TableCell></TableCell>
<TableCell></TableCell> <TableCell></TableCell>
<TableCell></TableCell> <TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell> <TableCell></TableCell>
<TableCell></TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{filteredStocks.map(stock => ( {filteredStocks.map(stock => {
<TableRow const today = new Date();
key={stock.stockId} const expDate = new Date(stock.expDate);
onClick={() => handleRowClick(stock)} const timeDiff = expDate.getTime() - today.getTime();
style={{ backgroundColor: selectedRow?.stockId === stock.stockId ? "yellow" : "white", cursor: "pointer" }} const daysLeft = Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
>
<TableCell>{stock.stuffName}</TableCell> return (
<TableCell>{stock.amount}</TableCell> <TableRow
<TableCell>{stock.price}</TableCell> key={stock.stockId}
<TableCell>{formatDate(stock.expDate)}</TableCell> onClick={() => handleRowClick(stock)}
<TableCell>{formatDate(stock.buyDate)}</TableCell> style={{ backgroundColor: selectedRow?.stockId === stock.stockId ? "yellow" : "white", cursor: "pointer" }}
</TableRow> >
))} <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" } : {}}
>
{formatDate(stock.expDate)}
</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
{/* 編集ダイアログ */} {/* 編集ダイアログ */}
<Dialog open={isEditOpen} onClose={handleCloseEdit} fullWidth maxWidth="sm"> <Dialog open={isEditOpen} onClose={handleCloseEdit} fullWidth maxWidth="sm">
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
@ -235,23 +315,23 @@ const StockPage: React.FC = () => {
onChange={handleChange} onChange={handleChange}
/> />
<TextField <TextField
label="賞味・消費期限 (yyyy-MM-dd)" label="購入日 (yyyy-MM-dd)"
fullWidth fullWidth
margin="normal" margin="normal"
name="expDate" name="buyDate"
value={editStock.expDate} value={editStock.buyDate}
onChange={handleChange} onChange={handleChange}
/> />
<TextField <TextField
label="購入日 (yyyy-MM-dd)" label="消費・賞味期限 (yyyy-MM-dd)"
fullWidth fullWidth
margin="normal" margin="normal"
name="buyDate" name="expDate"
value={editStock.buyDate} value={editStock.expDate}
onChange={handleChange} onChange={handleChange}
/> />
<Button onClick={() => {setIsEditOpen(false); setSelectedRow(null);}} sx={{ mt: 3, mb: 2, left: '68%' }}></Button> <Button onClick={() => { setIsEditOpen(false); setSelectedRow(null); }} sx={{ mt: 3, mb: 2, left: '68%' }}></Button>
<Button <Button
variant="contained" variant="contained"
color="success" color="success"
@ -277,9 +357,9 @@ const StockPage: React.FC = () => {
<DialogContent> <DialogContent>
{selectedRow && ( {selectedRow && (
<> <>
<Typography variant="h4">{selectedRow.stuffName}</Typography> <Typography variant="h4">{selectedRow.stuffName}</Typography>
<Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography> <Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography>
<Button onClick={() => {setIsDeleteOpen(false); setSelectedRow(null);}} sx={{ mt: 3, mb: 2, left: '70%' }}></Button> <Button onClick={() => { setIsDeleteOpen(false); setSelectedRow(null); }} sx={{ mt: 3, mb: 2, left: '70%' }}></Button>
<Button variant="contained" color="error" onClick={() => { <Button variant="contained" color="error" onClick={() => {
handleDeleteStock(selectedRow.stockId); handleDeleteStock(selectedRow.stockId);
setIsDeleteOpen(false); // 削除処理後にダイアログを閉じる setIsDeleteOpen(false); // 削除処理後にダイアログを閉じる
@ -290,8 +370,6 @@ const StockPage: React.FC = () => {
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </>
); );
}; };
@ -303,15 +381,138 @@ const StockPage: React.FC = () => {
</Typography> </Typography>
{/* タスク編集ボタン(全テーブル共通) */} {/* 在庫の食材追加ボタン */}
<Button variant="contained" color="primary" onClick={handleOpenAdd} sx={{ mt: 3, mb: 2, left: '78%' }}>
</Button>
{/* 新規タスク作成ダイアログ */}
<Dialog open={isAddOpen} onClose={() => setIsAddOpen(false)} 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)) {
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)) {
setNewStock({ ...newStock, price: parsedValue }); // number型で保存
}
}}
// sx={{ width: "50%" }}
type="number"
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} // ここで整数のみ許可
/>
{/* 購入日入力フィールド */}
<TextField
margin="dense"
label="購入日(yyyy/MM/dd)"
fullWidth
value={newStock.buyDate}
onChange={(e) => setNewStock({ ...newStock, buyDate: e.target.value })}
/>
{/* 賞味・消費期限入力フィールド */}
<TextField
margin="dense"
label="消費・賞味期限(yyyy/MM/dd)"
fullWidth
value={newStock.expDate}
onChange={(e) => setNewStock({ ...newStock, expDate: e.target.value })}
/>
</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, left: '80%' }}> <Button variant="contained" color="success" onClick={handleOpenEdit} sx={{ mt: 3, mb: 2, left: '80%' }}>
</Button> </Button>
{/* タスク削除ボタン */} {/* 在庫の食材削除ボタン (全テーブル共通) */}
<Button variant="contained" color="error" onClick={handleOpenDelete} sx={{ mt: 3, mb: 2, left: '82%' }}></Button> <Button variant="contained" color="error" onClick={handleOpenDelete} sx={{ mt: 3, mb: 2, left: '82%' }}></Button>
{/* 在庫一覧リスト */} {/* 在庫一覧リスト */}
{/* 乳製品 */} {/* 乳製品 */}
<Typography variant="h4" component="h1" gutterBottom></Typography> <Typography variant="h4" component="h1" gutterBottom></Typography>

@ -265,6 +265,40 @@ export const stockApi = {
}, },
/**
*
* @param stock
* @returns
*/
addStock: async (stock: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null }): Promise<{ result: boolean; message: string }> => {
console.log("送信するデータ:", stock); // 送信前のデータ確認
stock.buyDate = makeDateObject(stock.buyDate)?.toISOString()?.substring(0, 10) || ''
stock.expDate = makeDateObject(stock.expDate)?.toISOString()?.substring(0, 10) || ''
console.log("変換後のデータ:", stock); // 日付変換後のデータ確認
const response = await fetch(`${API_BASE_URL}/api/stocks/add`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify(stock),
});
console.log("レスポンスステータス:", response.status);
console.log("レスポンスヘッダー:", response.headers);
// console.log("レスポンス内容:", await response.text());
if (!response.ok) {
throw new Error(STOCK_ERRORS.CREATE_FAILED);
}
return response.json();
// return {result: true}
},
/** /**
* *
*/ */
@ -274,7 +308,7 @@ export const stockApi = {
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || ''
req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || '' req.expDate = makeDateObject(req.expDate)?.toISOString()?.substring(0, 10) || ''
console.log('req: ', req) console.log('req: ', req)
const response = await fetch(`${API_BASE_URL}/api/stocks/update`, { const response = await fetch(`${API_BASE_URL}/api/stocks/update`, {
method: 'PUT', method: 'PUT',

Loading…
Cancel
Save