購入時の数量入力の追加,購入履歴機能の実装

feature-backend-tobuy-history
Masaharu.Kato 4 months ago
parent c6cb147b89
commit befc9b6175
  1. 16
      backend/src/main/java/com/example/todoapp/config/InitTables.java
  2. 53
      backend/src/main/java/com/example/todoapp/controller/StocksController.java
  3. 2
      backend/src/main/java/com/example/todoapp/controller/ToBuysController.java
  4. 2
      backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java
  5. 12
      backend/src/main/java/com/example/todoapp/dto/BuyRequestDTO.java
  6. 4
      backend/src/main/java/com/example/todoapp/dto/StockDTO.java
  7. 42
      backend/src/main/java/com/example/todoapp/dto/StockHistoryDTO.java
  8. 4
      backend/src/main/java/com/example/todoapp/dto/StockResponseDTO.java
  9. 2
      backend/src/main/java/com/example/todoapp/dto/UpdateStockRequestDTO.java
  10. 14
      backend/src/main/java/com/example/todoapp/model/Stocks.java
  11. 4
      backend/src/main/java/com/example/todoapp/model/ToBuys.java
  12. 48
      backend/src/main/java/com/example/todoapp/repository/StocksRepository.java
  13. 19
      backend/src/main/java/com/example/todoapp/service/StocksService.java
  14. 10
      backend/src/main/java/com/example/todoapp/service/ToBuysService.java
  15. 9
      backend/src/main/resources/application-local.yml
  16. 26
      frontend/src/components/BuyDialog.tsx
  17. 105
      frontend/src/components/StuffHistoryDialog.tsx
  18. 2
      frontend/src/constants/errorMessages.ts
  19. 95
      frontend/src/pages/StockPage.tsx
  20. 57
      frontend/src/pages/TaskListPage.tsx
  21. 28
      frontend/src/services/api.ts
  22. 33
      frontend/src/types/types.ts

@ -15,10 +15,10 @@ import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
import com.example.todoapp.model.ToBuys;
import com.example.todoapp.repository.ToBuysRepository;
import com.example.todoapp.model.Stocks;
import com.example.todoapp.repository.StocksRepository;
// import com.example.todoapp.model.ToBuys;
// import com.example.todoapp.repository.ToBuysRepository;
// import com.example.todoapp.model.Stocks;
// import com.example.todoapp.repository.StocksRepository;
import com.example.todoapp.model.Recipes;
import com.example.todoapp.repository.RecipesRepository;
import com.example.todoapp.model.RecipeStuffs;
@ -29,10 +29,10 @@ import com.example.todoapp.repository.StuffsRepository;
@Configuration
public class InitTables {
@Autowired
private ToBuysRepository tobuysRepository;
@Autowired
private StocksRepository stocksRepository;
// @Autowired
// private ToBuysRepository tobuysRepository;
// @Autowired
// private StocksRepository stocksRepository;
@Autowired
private RecipesRepository recipesRepository;
@Autowired

@ -4,6 +4,7 @@ import com.example.todoapp.dto.AddStocksDTO;
import com.example.todoapp.dto.DeleteStockRequestDTO;
import com.example.todoapp.dto.StockResponseDTO;
import com.example.todoapp.dto.StockDTO;
import com.example.todoapp.dto.StockHistoryDTO;
import com.example.todoapp.dto.UpdateStockRequestDTO;
import com.example.todoapp.model.Stocks;
import com.example.todoapp.model.User;
@ -17,6 +18,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -41,6 +43,7 @@ public class StocksController {
/**
* ログインユーザーのすべての在庫を取得する
* 数量が 0 のものは省く
*
* @param authentication 認証情報
* @return ユーザーの在庫リスト
@ -63,11 +66,11 @@ public class StocksController {
* @return 在庫情報
*/
@GetMapping("/{id}")
public ResponseEntity<StockDTO> getStockById(
public ResponseEntity<StockResponseDTO> getStockById(
Authentication authentication,
@PathVariable("id") Long stockId) {
Stocks stock = stockService.getStockById(authentication.getName(), stockId);
return ResponseEntity.ok(StockDTO.fromEntity(stock));
return ResponseEntity.ok(StockResponseDTO.fromEntity(stock));
}
/**
@ -81,6 +84,11 @@ public class StocksController {
public ResponseEntity<StockDTO> createStock(
Authentication authentication,
@Valid @RequestBody AddStocksDTO stock) {
if (stock.getShop() == "") {
stock.setShop(null);
}
Stocks createdStock = stockService.createStock(authentication.getName(), stock);
return ResponseEntity.ok(StockDTO.fromEntity(createdStock));
}
@ -97,6 +105,10 @@ public class StocksController {
Authentication authentication,
@Valid @RequestBody UpdateStockRequestDTO updateStockRequest) {
if (updateStockRequest.getShop() == "") {
updateStockRequest.setShop(null);
}
Stocks updatedStock = stockService.updateStocks(authentication.getName(), updateStockRequest);
Map<String, Object> response = new HashMap<>();
@ -107,14 +119,15 @@ public class StocksController {
return ResponseEntity.ok(response);
}else {
response.put("result", true);
response.put("message", "更成功しました");
response.put("message", "更新に成功しました");
}
return ResponseEntity.ok(response);
}
/**
* 指定されたIDの在庫を削除する
* 指定されたIDの在庫を削除する
* 実際にレコードは削除せず数量を 0 に設定
*
* @param authentication 認証情報
* @param request 削除のリクエスト
@ -126,14 +139,13 @@ public class StocksController {
@RequestBody DeleteStockRequestDTO request
) {
// 認証されたユーザー名を取得
String username = authentication.getName();
Long stockId = request.getStockId();
// ユーザー情報を取得
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// 認証されたユーザー名を取得,ユーザーの在庫かどうか確かめる
String username = authentication.getName();
Stocks stock = stockService.getStockById(username, stockId);
int deletedCount = stockService.deleteStockById(user.getId(), request.getStockId());
int deletedCount = stockService.deleteStockById(stock.getStockId());
Map<String, Object> response = new HashMap<>();
@ -147,4 +159,25 @@ public class StocksController {
return ResponseEntity.ok(response);
}
@GetMapping("/getHistory")
public ResponseEntity<List<StockHistoryDTO>> getHistory(
Authentication authentication,
@RequestParam Long stuffId
) {
// ユーザー情報を取得
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// 材料IDを指定して在庫情報を取得
List<Stocks> stocks = stockService.getStockByStuffId(user.getId(), stuffId);
List<StockHistoryDTO> stockHistories = stocks.stream()
.map(StockHistoryDTO::fromEntity)
.collect(Collectors.toList());
return ResponseEntity.ok(stockHistories);
}
}

@ -119,7 +119,7 @@ public class ToBuysController {
resp.setStuffId(stuff.getStuffId());
resp.setStuffName(stuff.getStuffName());
resp.setAmount(toBuy.getAmount());
resp.setShop(toBuy.getStore());
resp.setShop(toBuy.getShop());
return resp;
})
.collect(Collectors.toList());

@ -24,7 +24,9 @@ public class AddStocksDTO {
private Long stuffId;
private String stuffName;
private int amount;
private Integer buyAmount;
private int price;
private String shop;
private String category;
private LocalDate buyDate;
private LocalDate lastUpdate;

@ -25,6 +25,18 @@ public class BuyRequestDTO {
*/
private int price;
/**
* 価格
* 実際の購入数量を設定します
*/
private int amount;
/**
* 店舗
* 購入店舗を設定します
*/
private String shop;
/**
* 消費期限
* 食材の消費期限を設定します

@ -20,7 +20,9 @@ public class StockDTO {
private Long stuffId;
private Long userId;
private int amount;
private Integer buyAmount;
private int price;
private String shop;
private LocalDate buyDate;
private LocalDate lastUpdate;
private LocalDate expDate;
@ -37,7 +39,9 @@ public class StockDTO {
dto.setStuffId(stock.getStuff().getStuffId());
dto.setUserId(stock.getUser().getId());
dto.setAmount(stock.getAmount());
dto.setBuyAmount(stock.getBuyAmount());
dto.setPrice(stock.getPrice());
dto.setShop(stock.getShop());
dto.setBuyDate(stock.getBuyDate());
dto.setLastUpdate(stock.getLastUpdate());
dto.setExpDate(stock.getExpDate());

@ -0,0 +1,42 @@
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 StockHistoryDTO {
private Long stockId;
private Long stuffId;
private Integer buyAmount;
private int price;
private String shop;
private LocalDate buyDate;
/**
* 在庫エンティティからDTOを作成する
*
* @param stock 変換元の在庫エンティティ
* @return 変換されたStockDTOオブジェクト
*/
public static StockHistoryDTO fromEntity(Stocks stock) {
StockHistoryDTO dto = new StockHistoryDTO();
dto.setStockId(stock.getStockId());
dto.setStuffId(stock.getStuff().getStuffId());
dto.setBuyAmount(stock.getBuyAmount());
dto.setPrice(stock.getPrice());
dto.setShop(stock.getShop());
dto.setBuyDate(stock.getBuyDate());
return dto;
}
}

@ -21,7 +21,9 @@ public class StockResponseDTO {
private Long stuffId;
private Long userId;
private int amount;
private Integer buyAmount;
private int price;
private String shop;
private LocalDate buyDate;
private LocalDate lastUpdate;
private LocalDate expDate;
@ -42,7 +44,9 @@ public class StockResponseDTO {
dto.setStuffId(stock.getStuff().getStuffId());
dto.setUserId(stock.getUser().getId());
dto.setAmount(stock.getAmount());
dto.setBuyAmount(stock.getBuyAmount());
dto.setPrice(stock.getPrice());
dto.setShop(stock.getShop());
dto.setBuyDate(stock.getBuyDate());
dto.setLastUpdate(stock.getLastUpdate());
dto.setExpDate(stock.getExpDate());

@ -15,7 +15,9 @@ import lombok.Data;
public class UpdateStockRequestDTO {
private Long stockId;
private int amount;
private Integer buyAmount;
private int price;
private String shop;
private LocalDate buyDate;
private LocalDate lastUpdate;
private LocalDate expDate;

@ -68,11 +68,23 @@ public class Stocks {
private int amount = 1;
/**
* シングルの値段デフォルト値:
* 購入時数量オプション
*/
@Column(nullable = true)
private Integer buyAmount;
/**
* 購入価格合計
*/
@Column(nullable = false)
private int price = 0;
/**
* 購入店舗オプション
*/
@Column(nullable = true)
private String shop;
/**
* 購入日
*/

@ -72,10 +72,10 @@ public class ToBuys {
private int amount = 1;
/**
* 購入するお
* 購入店
*/
@Column(nullable = false)
private String store;
private String shop;
}

@ -8,6 +8,7 @@
package com.example.todoapp.repository;
import com.example.todoapp.model.Stocks;
import com.example.todoapp.model.Stuffs;
import jakarta.transaction.Transactional;
@ -17,7 +18,9 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* 在庫エンティティのリポジトリインターフェース
@ -31,11 +34,13 @@ import java.util.List;
public interface StocksRepository extends JpaRepository<Stocks, Long> {
/**
* userIdから在庫一覧をstockId順で取得する
* 数量が 0 のものは除く
*
* @param userId 検索するユーザーID
* @return 在庫リスト
*/
List<Stocks> findStocksByUserIdOrderByStockIdAsc(Long userId);
@Query("SELECT s FROM Stocks s WHERE s.user.id = :userId AND s.amount <> 0 ORDER BY s.stockId DESC")
List<Stocks> findUserStocks(Long userId);
/**
* 在庫情報を更新する
@ -46,22 +51,47 @@ public interface StocksRepository extends JpaRepository<Stocks, Long> {
// boolean updateStocksByStockId(Stocks stock);
//updateをクエリ作成にて実装
@Modifying
@Transactional
@Query("UPDATE Stocks s SET s.amount = :#{#stock.amount}, s.price = :#{#stock.price}, s.buyDate = :#{#stock.buyDate}, s.lastUpdate = :#{#stock.lastUpdate}, s.expDate = :#{#stock.expDate} WHERE s.stockId = :#{#stock.stockId}")
@Query("UPDATE Stocks s SET s.amount = :#{#stock.amount}, s.buyAmount = :#{#stock.buyAmount}, s.price = :#{#stock.price}, s.shop = :#{#stock.shop}, s.buyDate = :#{#stock.buyDate}, s.lastUpdate = :#{#stock.lastUpdate}, s.expDate = :#{#stock.expDate} WHERE s.stockId = :#{#stock.stockId}")
int updateStocksById(@Param("stock") Stocks stock);
/**
* 在庫リストから指定した食材を削除する
* 在庫情報の数量を 0 に設定し削除したものとする
*
* @param stockId 削除する在庫
* @param userId 削除するユーザー
* @param stock 編集する新たな情報が入ったstockオブジェクト
* @return 編集に成功したらtrue
*/
// void deleteStocksByStockIdAndUserId(Long stockId, Long userId);
// boolean updateStocksByStockId(Stocks stock);
//updateをクエリ作成にて実装
@Modifying
@Transactional
@Query("DELETE FROM Stocks t WHERE t.user.id = :userId AND t.stockId = :stockId")
int deleteByUserIdAndStockId(@Param("userId") Long userId, @Param("stockId") Long stockId);
@Query("UPDATE Stocks s SET s.amount = 0, s.lastUpdate = :#{#lastUpdate} WHERE s.stockId = :#{#stockId}")
int updateStocksDeleted(@Param("stockId") Long stockId, @Param("lastUpdate") LocalDate lastUpdate);
// /**
// * 在庫リストから指定した食材を削除する
// * (実際にはレコードを削除しないので,このメソッドは使用しない)
// *
// * @param stockId 削除する在庫
// * @param userId 削除するユーザー
// */
// // void deleteStocksByStockIdAndUserId(Long stockId, Long userId);
// @Modifying
// @Transactional
// @Query("DELETE FROM Stocks t WHERE t.user.id = :userId AND t.stockId = :stockId")
// int deleteByUserIdAndStockId(@Param("userId") Long userId, @Param("stockId") Long stockId);
/**
* userId, stuffId を指定して購入日時順の新しい順に取得
*
* @param userId 検索するユーザーID
* @param stuffId 検索する材料ID
* @return 在庫リスト
*/
List<Stocks> findStocksByUserIdAndStuff_StuffIdOrderByBuyDateDescStockIdDesc(Long userId, Long stuffId);
}

@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@ -62,7 +63,9 @@ public class StocksService {
stockEntity.setStuff(stuffs);
stockEntity.setUser(user);
stockEntity.setAmount(stock.getAmount());
stockEntity.setBuyAmount(stock.getBuyAmount());
stockEntity.setPrice(stock.getPrice());
stockEntity.setShop(stock.getShop());
stockEntity.setBuyDate(stock.getBuyDate());
stockEntity.setLastUpdate(stock.getLastUpdate());
stockEntity.setExpDate(stock.getExpDate());
@ -77,7 +80,7 @@ public class StocksService {
*/
public List<Stocks> getALLStocksByUser(String username) {
User user = getUserByUsername(username);
return stocksRepository.findStocksByUserIdOrderByStockIdAsc(user.getId());
return stocksRepository.findUserStocks(user.getId());
}
/**
@ -105,7 +108,9 @@ public class StocksService {
public Stocks updateStocks(String username, UpdateStockRequestDTO stockDetails) {
Stocks stock = getStockById(username, stockDetails.getStockId());
stock.setAmount(stockDetails.getAmount());
stock.setBuyAmount(stockDetails.getBuyAmount());
stock.setPrice(stockDetails.getPrice());
stock.setShop(stockDetails.getShop());
stock.setLastUpdate(stockDetails.getLastUpdate());
stock.setBuyDate(stockDetails.getBuyDate());
stock.setExpDate(stockDetails.getExpDate());
@ -113,17 +118,23 @@ public class StocksService {
}
/**
* 指定された在庫を削除する
* 指定された在庫を削除する
* 実際にレコードは削除せず数量を 0 に設定
*
* @param userId ユーザー名
* @param stockId 削除する在庫のID
* @return 削除されたレコード数
*/
public int deleteStockById(Long userId, Long stockId) {
return stocksRepository.deleteByUserIdAndStockId(userId, stockId);
public int deleteStockById(Long stockId) {
LocalDate lastUpdate = LocalDate.now();
return stocksRepository.updateStocksDeleted(stockId, lastUpdate);
}
public List<Stocks> getStockByStuffId(Long userId, Long stuffId) {
return stocksRepository.findStocksByUserIdAndStuff_StuffIdOrderByBuyDateDescStockIdDesc(userId, stuffId);
}
/**
* ユーザー名からユーザーエンティティを取得する
*

@ -104,7 +104,7 @@ public class ToBuysService {
toBuys.setUser(user);
toBuys.setStuff(stuff);
toBuys.setAmount(toBuyDTO.getAmount());
toBuys.setStore(toBuyDTO.getShop());
toBuys.setShop(toBuyDTO.getShop());
return toBuysRepository.save(toBuys);
}
@ -155,7 +155,7 @@ public class ToBuysService {
toBuys.setUser(user);
toBuys.setStuff(stuffs);
toBuys.setAmount(toBuyDTO.getAmount());
toBuys.setStore(toBuyDTO.getShop());
toBuys.setShop(toBuyDTO.getShop());
// データベースに保存
return toBuysRepository.save(toBuys);
@ -199,8 +199,10 @@ public class ToBuysService {
Stocks stock = new Stocks();
stock.setStuff(tobuy.getStuff());
stock.setUser(user);
stock.setAmount(tobuy.getAmount());
stock.setAmount(dto.getAmount());
stock.setBuyAmount(dto.getAmount()); // 現在の数量 = 購入数量
stock.setPrice(dto.getPrice());
stock.setShop(dto.getShop());
stock.setLastUpdate(dto.getLastUpdate());
stock.setBuyDate(dto.getBuyDate());
stock.setExpDate(dto.getExpDate());
@ -248,7 +250,7 @@ public class ToBuysService {
toBuy.setUser(user);
toBuy.setStuff(stuff);
toBuy.setAmount(requiredAmount);
toBuy.setStore("");
toBuy.setShop("");
}
toBuy = toBuysRepository.save(toBuy);

@ -13,3 +13,12 @@ spring:
non_contextual_creation: true
cors:
allowed-origins: http://localhost:3000
logging:
level:
org.hibernate.hql.internal.ast.ErrorCounter: DEBUG
org.hibernate.hql.internal.ast.QueryTranslatorImpl: DEBUG
org.springframework.data.jpa.repository.query: DEBUG
org.hibernate.hql.internal.antlr: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type: TRACE

@ -21,7 +21,7 @@ const formatDateLocal = (date: Date) => {
};
// 日本語ロケールを登録
registerLocale('ja', ja);
registerLocale('ja', ja);
const BuyDialog = ({
openDialog,
@ -43,7 +43,7 @@ const BuyDialog = ({
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }}
>
>
<DialogTitle></DialogTitle>
<DialogContent>
<Box sx={{ pt: 1 }}>
@ -54,7 +54,7 @@ const BuyDialog = ({
fullWidth
value={stuffName}
disabled
sx={{ marginBottom: 2 , marginTop: 2}}
sx={{ marginBottom: 2, marginTop: 2 }}
/>
{/* 価格入力フィールド */}
@ -72,6 +72,23 @@ const BuyDialog = ({
}}
sx={{ marginBottom: 2 }}
/>
{/* 価格入力フィールド */}
<TextField
autoFocus
margin="dense"
label="購入数量"
fullWidth
value={newStock.amount}
onChange={(e) => {
const value = e.target.value;
if (/^\d*$/.test(value)) {
setNewStock({ ...newStock, amount: value })
};
}}
sx={{ marginBottom: 2 }}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日入力フィールド */}
@ -121,10 +138,11 @@ const BuyDialog = ({
/>
</Box>
{/* 購入店舗入力フィールド */}
{/* TODO: 実装 */}
<TextField
margin="dense"
label="店舗"
value={newStock.shop}
onChange={(e) => setNewStock({...newStock, shop: e.target.value})}
fullWidth
/>
</Box>

@ -0,0 +1,105 @@
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
IconButton, // 追加: 閉じるボタンのためにIconButtonをインポート
Table, // 追加: テーブル表示のためにTableをインポート
TableBody, // 追加
TableCell, // 追加
TableContainer, // 追加
TableHead, // 追加
TableRow, // 追加
Paper,
Typography, // 追加: TableContainerの背景用
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close'; // 追加: 閉じるアイコンをインポート
import { StockHistory } from '../types/types';
const StuffHistoryDialog = ({
openDialog,
setOpenDialog,
stuffName,
stockHistories,
}: {
openDialog: boolean,
setOpenDialog: (open: boolean) => void,
stuffName: string,
stockHistories: StockHistory[],
}) => {
return (
<Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '300px', maxHeight: '80vh' } }}>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
: {stuffName}
<IconButton
aria-label="close"
onClick={() => setOpenDialog(false)}
sx={{
position: 'absolute',
right: 8,
top: 8,
color: (theme) => theme.palette.grey[500],
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ padding: 0 }}> {/* dividersを追加して区切り線を表示, 表をぴったりくっつける */}
{(!stockHistories.length ?
<Typography sx={{ margin: "1rem" }}></Typography>
:
<TableContainer
component={Paper}
sx={{
boxShadow: 'none',
margin: 0,
borderRadius: 0,
// TableContainerに横スクロールを有効にする
overflowX: 'auto',
}}
>
<Table sx={{ minWidth: 400 }} aria-label="purchase history table">
<TableHead sx={{ backgroundColor: "#dcdcdc", color: "#333" }}>
<TableRow>
{/* 各ヘッダーセルに white-space: nowrap; を適用 */}
<TableCell sx={{ whiteSpace: 'nowrap' }}></TableCell>
{/* 「購入店舗」ヘッダーも nowrap にし、minWidth でスクロールを考慮 */}
<TableCell sx={{ whiteSpace: 'nowrap', minWidth: '150px' }}></TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{stockHistories.map((history) => (
<TableRow
key={history.stockId}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{/* 各ボディセルに white-space: nowrap; を適用 */}
<TableCell component="th" scope="row" sx={{ whiteSpace: 'nowrap' }}>
{history.buyDate}
</TableCell>
{/* 「購入店舗」セルに nowrap と minWidth を適用 */}
<TableCell sx={{ whiteSpace: 'nowrap', minWidth: '150px' }}>{history.shop || ''}</TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}>{history.buyAmount}</TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}>{history.price}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</DialogContent>
{/* 必要であればDialogActionsにボタンなどを追加できます */}
{/* <DialogActions>
<Button onClick={() => setOpenDialog(false)}></Button>
</DialogActions> */}
</Dialog>
);
}
export default StuffHistoryDialog;

@ -6,6 +6,8 @@
// 一般的なエラーメッセージ
export const GENERAL_ERRORS = {
UNEXPECTED_ERROR: '予期せぬエラーが発生しました',
INVALID_AMOUNT: '数量が正しく入力されていません。',
INVALID_PRICE: '価格が正しく入力されていません。',
};
// 認証関連のエラーメッセージ

@ -4,7 +4,7 @@
*/
import React, { useState, useEffect } from 'react';
import { stockApi, stuffApi } from '../services/api';
import { Stock, Stuff } from '../types/types';
import { Stock, StockUpdateRequest, Stuff } from '../types/types';
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material";
import {
Container,
@ -26,7 +26,7 @@ import {
Select,
MenuItem,
} from '@mui/material';
import { STOCK_ERRORS } from '../constants/errorMessages';
import { GENERAL_ERRORS, 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';
@ -44,7 +44,9 @@ const EMPTY_STOCK: Omit<Stock, 'stockId' | 'stuffId'> & { stuffId: number | null
stuffId: null,
stuffName: '',
amount: 1,
buyAmount: 1,
price: 0,
shop: '',
lastUpdate: '',
buyDate: new Date().toISOString(),
expDate: '',
@ -73,7 +75,7 @@ const StockPage: React.FC = () => {
// 在庫の編集状態
const [editStock, setEditStock] = useState<Stock | null>(null);
const { showWarningMessage } = useMessage();
const { showErrorMessage, showWarningMessage } = useMessage();
// コンポーネントマウント時にタスク一覧を取得
useEffect(() => {
@ -91,6 +93,7 @@ const StockPage: React.FC = () => {
setStocks(stocks);
} catch (error) {
console.error(`${STOCK_ERRORS.FETCH_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.FETCH_FAILED);
}
};
@ -105,6 +108,11 @@ const StockPage: React.FC = () => {
if (isNaN(newStock.amount)) return;
if (isNaN(newStock.price)) return;
if (newStock.buyAmount !== null && isNaN(newStock.buyAmount)) {
newStock.buyAmount = null;
}
newStock.shop = newStock.shop || null;
console.log(newStock)
const today = new Date().toISOString().substring(0, 10);
@ -119,19 +127,20 @@ const StockPage: React.FC = () => {
fetchStocks(); // 作成後のタスク一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.CREATE_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.CREATE_FAILED);
}
};
/**
*
*/
const handleUpdateStock = async (stockId: number, amount: number, price: number, buyDate: string, expDate: string) => {
const handleUpdateStock = async (request: StockUpdateRequest) => {
try {
const today = new Date().toISOString().substring(0, 10);
await stockApi.updateStock({ stockId, amount, price, lastUpdate: today, buyDate, expDate });
await stockApi.updateStock(request);
fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.UPDATE_FAILED);
}
};
@ -145,6 +154,7 @@ const StockPage: React.FC = () => {
fetchStocks(); // 削除後の買うもの一覧を再取得
} catch (error) {
console.error(`${STOCK_ERRORS.DELETE_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.DELETE_FAILED);
}
};
@ -211,28 +221,48 @@ const StockPage: React.FC = () => {
const handleApplyChanges = async () => {
if (!editStock) return;
const {stockId, amount, buyAmount, price, shop, buyDate, expDate, lastUpdate} = editStock;
try {
if (Number(editStock.amount) === 0) {
if (amount === 0) {
// 数量が 0 の場合は削除処理へ誘導
setIsEditOpen(false); // 編集ダイアログを閉じる
setSelectedRow(editStock); // 削除対象をセット
setIsDeleteOpen(true); // 削除ダイアログを開く
return;
}
if (!amount || !buyAmount) {
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return;
}
if (!price) {
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE);
return;
}
await handleUpdateStock(
editStock.stockId,
Number(editStock.amount),
Number(editStock.price),
editStock.buyDate,
editStock.expDate
);
const lastUpdate = new Date().toISOString().substring(0, 10);
const updateRequest = {
stockId,
amount,
buyAmount,
price,
shop,
buyDate,
expDate,
lastUpdate,
}
console.log('updateRequest:', updateRequest);
await handleUpdateStock(updateRequest);
setSelectedRow(editStock); // 更新後に選択行を反映
fetchStocks(); // 最新データを取得
setSelectedRow(null); // 選択解除
} catch (error) {
console.error(`${STOCK_ERRORS.UPDATE_FAILED}:`, error);
showErrorMessage(STOCK_ERRORS.UPDATE_FAILED);
}
setIsEditOpen(false); // 編集ダイアログを閉じる
@ -251,8 +281,10 @@ const StockPage: React.FC = () => {
const { name, value } = event.target;
// 数値項目に対して負の値をブロック
const numericFields = ['amount', 'price'];
if (numericFields.includes(name) && Number(value) < 0) {
const numericFields = ['amount', 'buyAmount', 'price'];
const numericValue = Number(value);
const isNumericField = numericFields.includes(name);
if (isNumericField && numericValue < 0) {
return; // 無視して更新しない
}
@ -344,9 +376,12 @@ const StockPage: React.FC = () => {
<DialogContent>
{editStock && (
<>
{/* 材料名 */}
<Typography variant="h4">{editStock.stuffName}</Typography>
<TextField
label="数量"
label="現在の数量"
fullWidth
margin="normal"
name="amount"
@ -359,8 +394,24 @@ const StockPage: React.FC = () => {
e.preventDefault();
}
}}
/>
<TextField
label="購入時数量"
fullWidth
margin="normal"
name="buyAmount"
type="number"
value={editStock.buyAmount}
onChange={handleChange}
inputProps={{ min: 0 }}
onKeyDown={(e) => {
if (e.key === '-' || e.key === 'e' || e.key === 'E') {
e.preventDefault();
}
}}
/>
<TextField
label="購入価格"
fullWidth
@ -375,8 +426,18 @@ const StockPage: React.FC = () => {
e.preventDefault();
}
}}
/>
<TextField
label="購入店舗"
fullWidth
margin="normal"
name="shop"
type="text"
value={editStock.shop}
onChange={handleChange}
/>
{/* 購入日・消費期限を横並びに */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
{/* 購入日 */}

@ -3,7 +3,7 @@
*
*/
import React, { useState, useEffect } from 'react';
import { toBuyApi } from '../services/api';
import { stockApi, toBuyApi } from '../services/api';
import {
Container,
Typography,
@ -24,14 +24,15 @@ import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon, Edit as EditIcon
} from '@mui/icons-material';
import { ToBuy, Stuff, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, /*Stock*/ } from '../types/types';
import { TOBUY_ERRORS } from '../constants/errorMessages';
import { ToBuy, Stuff, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, StockHistory, /*Stock*/ } from '../types/types';
import { GENERAL_ERRORS, TOBUY_ERRORS } from '../constants/errorMessages';
import EditAmountDialog from '../components/EditAmountDialog';
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 StuffHistoryDialog from '../components/StuffHistoryDialog';
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
@ -65,7 +66,9 @@ const TaskListPage: React.FC = () => {
const [selectedToBuyId, setSelectedToBuyId] = useState<ToBuy["tobuyId"]>(0);
const [newStock, setNewStock] = useState<NewStock>({
amount: '', // 購入数量(ここではstring)
price: '', // ここではstring
shop: '',
buyDate: new Date().toISOString(),
expDate: '',
});
@ -75,6 +78,7 @@ const TaskListPage: React.FC = () => {
const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0);
// 編集対象の項目
const [editingItem, setEditingItem] = useState<ToBuy>({
tobuyId: 0,
stuffId: 0,
@ -150,7 +154,7 @@ const TaskListPage: React.FC = () => {
const handleAddNewToBuy = async () => {
try {
if (isNaN(newToBuy.amount)) {
showErrorMessage('数量が正しくありません.');
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return;
}
@ -172,7 +176,7 @@ const TaskListPage: React.FC = () => {
const handleUpdateNewToBuy = async () => {
try {
if (isNaN(editingItem.amount)) {
showErrorMessage('数量が正しくありません.');
showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return;
}
@ -196,11 +200,22 @@ const TaskListPage: React.FC = () => {
console.log("newPrice:", newStock.price)
console.log("parsedPrice: ", parsedPrice)
if (isNaN(parsedPrice)) {
showErrorMessage('価格が正しく入力されていません。')
showErrorMessage(GENERAL_ERRORS.INVALID_PRICE)
return
//setNewStock({ ...newStock, price: parsedPrice });
}
await toBuyApi.buy({ tobuyId: selectedToBuyId, ...newStock, price: parsedPrice, lastUpdate: today }); //データベースに送信
const amount = parseInt(newStock.amount, 10);
if (isNaN(amount)) {
showErrorMessage('購入数量が正しく入力されていません。')
return
}
await toBuyApi.buy({
tobuyId: selectedToBuyId,
...newStock,
amount,
price: parsedPrice,
lastUpdate: today
}); //データベースに送信
setOpenInfoDialog(false);
fetchTasks(); // 変更後後の買うもの一覧を再取得
} catch (error) {
@ -208,6 +223,23 @@ const TaskListPage: React.FC = () => {
}
};
//履歴表示ダイアログ
const [openHistoryDialog, setOpenHistoryDialog] = useState(false);
const [historyTobuy, setHistoryTobuy] = useState<ToBuy | null>(null);
const [stockHistories, setStockHistories] = useState<StockHistory[] | null>(null);
const handleShowHistories = async (tobuy: ToBuy) => {
console.log('handleShowHistories:', tobuy);
try {
setHistoryTobuy(tobuy);
setStockHistories(await stockApi.getHistories(tobuy.stuffId));
setOpenHistoryDialog(true);
} catch {
showErrorMessage("履歴の読み込みに失敗しました。");
}
}
return (
<Container>
<Typography variant="h4" component="h1" gutterBottom>
@ -230,6 +262,7 @@ const TaskListPage: React.FC = () => {
{/* 食材名 */}
<ListItemText
primary={tobuy.stuffName}
onClick={() => handleShowHistories(tobuy)}
/>
{/* 買い物リスト:食材情報記入ボタン */}
<ListItemSecondaryAction>
@ -253,6 +286,7 @@ const TaskListPage: React.FC = () => {
onClick={() => {
setOpenInfoDialog(true)
setEditingItem(tobuy)
setNewStock({ ...newStock, amount: String(tobuy.amount) });
setSelectedToBuyId(tobuy.tobuyId)
// handleDeleteTask(tobuy.tobuyId)
}}>
@ -337,7 +371,7 @@ const TaskListPage: React.FC = () => {
{/* 数量変更ダイアログ */}
<EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog}
editingItem={editingItem}
setEditingItem={(v) => setEditingItem({...editingItem, ...v})}
setEditingItem={(v) => setEditingItem({ ...editingItem, ...v })}
onSubmit={handleUpdateNewToBuy} />
{/* 削除ダイアログ */}
@ -362,6 +396,13 @@ const TaskListPage: React.FC = () => {
)}
</DialogContent>
</Dialog>
{/* 履歴表示ダイアログ */}
{
(historyTobuy !== null && stockHistories !== null) &&
<StuffHistoryDialog openDialog={openHistoryDialog} setOpenDialog={setOpenHistoryDialog} stuffName={historyTobuy.stuffName} stockHistories={stockHistories} />
}
</Container>
);

@ -3,7 +3,7 @@
* APIとの通信を担当するモジュール
*
*/
import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId } from '../types/types';
import { LoginCredentials, RegisterCredentials, AuthResponse, /* Task, */ ToBuy, Stuff, Stock, RecipeDetail, StuffAndCategoryAndAmount, RecipeWithId, StockHistory, StockUpdateRequest } from '../types/types';
import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用
@ -188,9 +188,9 @@ export const toBuyApi = {
/**
*
*/
buy: async (req: { tobuyId: number, price: number, expDate: string, buyDate: string, lastUpdate: string }): Promise<{ result: boolean }> => {
buy: async (req: { tobuyId: number, amount: number, price: number, shop: string, expDate: string, buyDate: string, lastUpdate: string }): Promise<{ result: boolean }> => {
console.log('req: ', req)
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) || ''
@ -314,7 +314,7 @@ export const stockApi = {
/**
*
*/
updateStock: async (req: { stockId: number, amount: number, price: number, lastUpdate: string, buyDate: string, expDate: string }): Promise<{ result: boolean; message: string }> => {
updateStock: async (req: StockUpdateRequest): Promise<{ result: boolean; message: string }> => {
// console.log('req: ', req)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || ''
@ -366,7 +366,25 @@ export const stockApi = {
// }
},
}
/**
*
* @param recipeId ID
* @returns
*/
getHistories: async (stuffId: number): Promise<StockHistory[]> => {
const response = await fetch(`${API_BASE_URL}/api/stocks/getHistory?stuffId=${stuffId}`, {
method: 'GET',
headers: getHeaders(),
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.message);
}
return response.json();
},
};
/**
* API機能を提供するオブジェクト

@ -58,27 +58,44 @@ export interface Stuff {
category: string,
}
/**
*
*
*/
export interface Stock {
export interface StockUpdateRequest {
stockId: number,
stuffId: number,
stuffName: string,
amount: number,
buyAmount: number | null,
price: number,
shop: string | null,
buyDate: string,
lastUpdate: string,
expDate: string,
lastUpdate: string,
}
/**
*
*/
export interface Stock extends StockUpdateRequest {
stuffId: number,
stuffName: string,
category: string,
}
/**
*
*/
export interface StockHistory {
stockId: number,
buyAmount: number,
price: number,
shop: number | null,
buyDate: string,
}
/**
*
*/
export interface NewStock {
amount: string,
price: string,
shop: string,
buyDate: string,
expDate: string,
}

Loading…
Cancel
Save