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

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. 55
      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. 32
      backend/src/main/java/com/example/todoapp/model/Stocks.java
  11. 4
      backend/src/main/java/com/example/todoapp/model/ToBuys.java
  12. 50
      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. 11
      backend/src/main/resources/application-local.yml
  16. 230
      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. 67
      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 jakarta.annotation.PostConstruct;
import com.example.todoapp.model.ToBuys; // import com.example.todoapp.model.ToBuys;
import com.example.todoapp.repository.ToBuysRepository; // import com.example.todoapp.repository.ToBuysRepository;
import com.example.todoapp.model.Stocks; // import com.example.todoapp.model.Stocks;
import com.example.todoapp.repository.StocksRepository; // import com.example.todoapp.repository.StocksRepository;
import com.example.todoapp.model.Recipes; import com.example.todoapp.model.Recipes;
import com.example.todoapp.repository.RecipesRepository; import com.example.todoapp.repository.RecipesRepository;
import com.example.todoapp.model.RecipeStuffs; import com.example.todoapp.model.RecipeStuffs;
@ -29,10 +29,10 @@ import com.example.todoapp.repository.StuffsRepository;
@Configuration @Configuration
public class InitTables { public class InitTables {
@Autowired // @Autowired
private ToBuysRepository tobuysRepository; // private ToBuysRepository tobuysRepository;
@Autowired // @Autowired
private StocksRepository stocksRepository; // private StocksRepository stocksRepository;
@Autowired @Autowired
private RecipesRepository recipesRepository; private RecipesRepository recipesRepository;
@Autowired @Autowired

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

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

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

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

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

@ -40,17 +40,17 @@ public class Stocks {
private Long stockId; private Long stockId;
/** /**
* 商品テーブル参照用の外部キー * 商品テーブル参照用の外部キー
*/ */
@NotNull @NotNull
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn( @JoinColumn(
name = "stuffId", name = "stuffId",
referencedColumnName = "stuffId", referencedColumnName = "stuffId",
nullable = false nullable = false
) )
private Stuffs stuff; private Stuffs stuff;
/** /**
* タスクの所有者ユーザー * タスクの所有者ユーザー
@ -68,11 +68,23 @@ public class Stocks {
private int amount = 1; private int amount = 1;
/** /**
* シングルの値段デフォルト値: * 購入時数量オプション
*/
@Column(nullable = true)
private Integer buyAmount;
/**
* 購入価格合計
*/ */
@Column(nullable = false) @Column(nullable = false)
private int price = 0; private int price = 0;
/**
* 購入店舗オプション
*/
@Column(nullable = true)
private String shop;
/** /**
* 購入日 * 購入日
*/ */

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

@ -8,6 +8,7 @@
package com.example.todoapp.repository; package com.example.todoapp.repository;
import com.example.todoapp.model.Stocks; import com.example.todoapp.model.Stocks;
import com.example.todoapp.model.Stuffs;
import jakarta.transaction.Transactional; 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.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* 在庫エンティティのリポジトリインターフェース * 在庫エンティティのリポジトリインターフェース
@ -31,11 +34,13 @@ import java.util.List;
public interface StocksRepository extends JpaRepository<Stocks, Long> { public interface StocksRepository extends JpaRepository<Stocks, Long> {
/** /**
* userIdから在庫一覧をstockId順で取得する * userIdから在庫一覧をstockId順で取得する
* 数量が 0 のものは除く
* *
* @param userId 検索するユーザーID * @param userId 検索するユーザーID
* @return 在庫リスト * @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); // boolean updateStocksByStockId(Stocks stock);
//updateをクエリ作成にて実装 //updateをクエリ作成にて実装
@Modifying @Modifying
@Transactional @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); int updateStocksById(@Param("stock") Stocks stock);
/** /**
* 在庫リストから指定した食材を削除する * 在庫情報の数量を 0 に設定し削除したものとする
* *
* @param stockId 削除する在庫 * @param stock 編集する新たな情報が入ったstockオブジェクト
* @param userId 削除するユーザー * @return 編集に成功したらtrue
*/ */
// void deleteStocksByStockIdAndUserId(Long stockId, Long userId); // boolean updateStocksByStockId(Stocks stock);
//updateをクエリ作成にて実装
@Modifying @Modifying
@Transactional @Transactional
@Query("DELETE FROM Stocks t WHERE t.user.id = :userId AND t.stockId = :stockId") @Query("UPDATE Stocks s SET s.amount = 0, s.lastUpdate = :#{#lastUpdate} WHERE s.stockId = :#{#stockId}")
int deleteByUserIdAndStockId(@Param("userId") Long userId, @Param("stockId") Long 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.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -62,7 +63,9 @@ public class StocksService {
stockEntity.setStuff(stuffs); stockEntity.setStuff(stuffs);
stockEntity.setUser(user); stockEntity.setUser(user);
stockEntity.setAmount(stock.getAmount()); stockEntity.setAmount(stock.getAmount());
stockEntity.setBuyAmount(stock.getBuyAmount());
stockEntity.setPrice(stock.getPrice()); stockEntity.setPrice(stock.getPrice());
stockEntity.setShop(stock.getShop());
stockEntity.setBuyDate(stock.getBuyDate()); stockEntity.setBuyDate(stock.getBuyDate());
stockEntity.setLastUpdate(stock.getLastUpdate()); stockEntity.setLastUpdate(stock.getLastUpdate());
stockEntity.setExpDate(stock.getExpDate()); stockEntity.setExpDate(stock.getExpDate());
@ -77,7 +80,7 @@ public class StocksService {
*/ */
public List<Stocks> getALLStocksByUser(String username) { public List<Stocks> getALLStocksByUser(String username) {
User user = getUserByUsername(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) { public Stocks updateStocks(String username, UpdateStockRequestDTO stockDetails) {
Stocks stock = getStockById(username, stockDetails.getStockId()); Stocks stock = getStockById(username, stockDetails.getStockId());
stock.setAmount(stockDetails.getAmount()); stock.setAmount(stockDetails.getAmount());
stock.setBuyAmount(stockDetails.getBuyAmount());
stock.setPrice(stockDetails.getPrice()); stock.setPrice(stockDetails.getPrice());
stock.setShop(stockDetails.getShop());
stock.setLastUpdate(stockDetails.getLastUpdate()); stock.setLastUpdate(stockDetails.getLastUpdate());
stock.setBuyDate(stockDetails.getBuyDate()); stock.setBuyDate(stockDetails.getBuyDate());
stock.setExpDate(stockDetails.getExpDate()); stock.setExpDate(stockDetails.getExpDate());
@ -113,17 +118,23 @@ public class StocksService {
} }
/** /**
* 指定された在庫を削除する * 指定された在庫を削除する
* 実際にレコードは削除せず数量を 0 に設定
* *
* @param userId ユーザー名 * @param userId ユーザー名
* @param stockId 削除する在庫のID * @param stockId 削除する在庫のID
* @return 削除されたレコード数 * @return 削除されたレコード数
*/ */
public int deleteStockById(Long userId, Long stockId) { public int deleteStockById(Long stockId) {
return stocksRepository.deleteByUserIdAndStockId(userId, 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.setUser(user);
toBuys.setStuff(stuff); toBuys.setStuff(stuff);
toBuys.setAmount(toBuyDTO.getAmount()); toBuys.setAmount(toBuyDTO.getAmount());
toBuys.setStore(toBuyDTO.getShop()); toBuys.setShop(toBuyDTO.getShop());
return toBuysRepository.save(toBuys); return toBuysRepository.save(toBuys);
} }
@ -155,7 +155,7 @@ public class ToBuysService {
toBuys.setUser(user); toBuys.setUser(user);
toBuys.setStuff(stuffs); toBuys.setStuff(stuffs);
toBuys.setAmount(toBuyDTO.getAmount()); toBuys.setAmount(toBuyDTO.getAmount());
toBuys.setStore(toBuyDTO.getShop()); toBuys.setShop(toBuyDTO.getShop());
// データベースに保存 // データベースに保存
return toBuysRepository.save(toBuys); return toBuysRepository.save(toBuys);
@ -199,8 +199,10 @@ public class ToBuysService {
Stocks stock = new Stocks(); Stocks stock = new Stocks();
stock.setStuff(tobuy.getStuff()); stock.setStuff(tobuy.getStuff());
stock.setUser(user); stock.setUser(user);
stock.setAmount(tobuy.getAmount()); stock.setAmount(dto.getAmount());
stock.setBuyAmount(dto.getAmount()); // 現在の数量 = 購入数量
stock.setPrice(dto.getPrice()); stock.setPrice(dto.getPrice());
stock.setShop(dto.getShop());
stock.setLastUpdate(dto.getLastUpdate()); stock.setLastUpdate(dto.getLastUpdate());
stock.setBuyDate(dto.getBuyDate()); stock.setBuyDate(dto.getBuyDate());
stock.setExpDate(dto.getExpDate()); stock.setExpDate(dto.getExpDate());
@ -248,7 +250,7 @@ public class ToBuysService {
toBuy.setUser(user); toBuy.setUser(user);
toBuy.setStuff(stuff); toBuy.setStuff(stuff);
toBuy.setAmount(requiredAmount); toBuy.setAmount(requiredAmount);
toBuy.setStore(""); toBuy.setShop("");
} }
toBuy = toBuysRepository.save(toBuy); toBuy = toBuysRepository.save(toBuy);

@ -12,4 +12,13 @@ spring:
lob: lob:
non_contextual_creation: true non_contextual_creation: true
cors: cors:
allowed-origins: http://localhost:3000 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

@ -1,12 +1,12 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
TextField, TextField,
Button, Button,
Box, Box,
} from '@mui/material'; } from '@mui/material';
import { NewStock } from '../types/types'; import { NewStock } from '../types/types';
import DatePicker, { registerLocale } from 'react-datepicker'; import DatePicker, { registerLocale } from 'react-datepicker';
@ -21,78 +21,95 @@ const formatDateLocal = (date: Date) => {
}; };
// 日本語ロケールを登録 // 日本語ロケールを登録
registerLocale('ja', ja); registerLocale('ja', ja);
const BuyDialog = ({ const BuyDialog = ({
openDialog, openDialog,
setOpenDialog, setOpenDialog,
stuffName, stuffName,
newStock, newStock,
setNewStock, setNewStock,
onSubmit, onSubmit,
}: { }: {
openDialog: boolean, openDialog: boolean,
setOpenDialog: (open: boolean) => void, setOpenDialog: (open: boolean) => void,
stuffName: string, stuffName: string,
newStock: NewStock, newStock: NewStock,
setNewStock: (tobuy: NewStock) => void, setNewStock: (tobuy: NewStock) => void,
onSubmit: () => void, onSubmit: () => void,
}) => { }) => {
return (
return (
<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}}
/>
{/* 価格入力フィールド */} <Dialog open={openDialog} onClose={() => setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }}
<TextField >
autoFocus <DialogTitle></DialogTitle>
margin="dense" <DialogContent>
label="価格" <Box sx={{ pt: 1 }}>
fullWidth {/* 材料名表示 */}
value={newStock.price} <TextField
onChange={(e) => { margin="dense"
const value = e.target.value; label="材料名"
if (/^\d*$/.test(value)) { fullWidth
setNewStock({ ...newStock, price: value }) value={stuffName}
}; disabled
}} sx={{ marginBottom: 2, marginTop: 2 }}
sx={{ marginBottom: 2 }} />
/>
{/* 購入日・消費期限を横並びに */} {/* 価格入力フィールド */}
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}> <TextField
{/* 購入日入力フィールド */} autoFocus
<DatePicker margin="dense"
popperClassName="custom-datepicker-popper" label="価格"
selected={newStock.buyDate ? new Date(newStock.buyDate) : null} fullWidth
onChange={(date) => value={newStock.price}
setNewStock({ ...newStock, buyDate: date ? formatDateLocal(date) : '' }) onChange={(e) => {
} const value = e.target.value;
dateFormat="yyyy/MM/dd" if (/^\d*$/.test(value)) {
customInput={ setNewStock({ ...newStock, price: value })
<TextField };
margin="dense" }}
label="購入日(yyyy/MM/dd)" sx={{ marginBottom: 2 }}
fullWidth />
/>
} {/* 価格入力フィールド */}
isClearable <TextField
//withPortal // ← 他の文字との重なり対策 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 }}>
{/* 購入日入力フィールド */}
<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 <TextField
margin="dense" margin="dense"
label="購入日(yyyy/MM/dd)" label="購入日(yyyy/MM/dd)"
@ -101,41 +118,42 @@ const BuyDialog = ({
onChange={(e) => setNewStock({ ...newStock, buyDate: e.target.value })} onChange={(e) => setNewStock({ ...newStock, buyDate: e.target.value })}
/> />
*/} */}
{/* 消費・賞味期限入力フィールド */} {/* 消費・賞味期限入力フィールド */}
<DatePicker <DatePicker
popperClassName="custom-datepicker-popper" popperClassName="custom-datepicker-popper"
selected={newStock.expDate ? new Date(newStock.expDate) : null} selected={newStock.expDate ? new Date(newStock.expDate) : null}
onChange={(date) => onChange={(date) =>
setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' }) setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' })
} }
dateFormat="yyyy/MM/dd" dateFormat="yyyy/MM/dd"
customInput={ customInput={
<TextField <TextField
margin="dense" margin="dense"
label="消費・賞味期限(yyyy/MM/dd)" label="消費・賞味期限(yyyy/MM/dd)"
fullWidth fullWidth
/> />
} }
isClearable isClearable
//withPortal //withPortal
/> />
</Box> </Box>
{/* 購入店舗入力フィールド */} {/* 購入店舗入力フィールド */}
{/* TODO: 実装 */} <TextField
<TextField margin="dense"
margin="dense" label="店舗"
label="店舗" value={newStock.shop}
fullWidth onChange={(e) => setNewStock({...newStock, shop: e.target.value})}
/> fullWidth
</Box> />
</DialogContent> </Box>
<DialogActions> </DialogContent>
<Button onClick={() => setOpenDialog(false)}></Button> <DialogActions>
<Button onClick={() => { onSubmit() }} variant="contained"></Button> <Button onClick={() => setOpenDialog(false)}></Button>
</DialogActions> <Button onClick={() => { onSubmit() }} variant="contained"></Button>
</Dialog> </DialogActions>
</Dialog>
) )
} }
export default BuyDialog; export default BuyDialog;

@ -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 = { export const GENERAL_ERRORS = {
UNEXPECTED_ERROR: '予期せぬエラーが発生しました', UNEXPECTED_ERROR: '予期せぬエラーが発生しました',
INVALID_AMOUNT: '数量が正しく入力されていません。',
INVALID_PRICE: '価格が正しく入力されていません。',
}; };
// 認証関連のエラーメッセージ // 認証関連のエラーメッセージ

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

@ -3,7 +3,7 @@
* *
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { toBuyApi } from '../services/api'; import { stockApi, toBuyApi } from '../services/api';
import { import {
Container, Container,
Typography, Typography,
@ -24,14 +24,15 @@ import {
Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon, Add as AddIcon, Delete as DeleteIcon, ShoppingBasket as ShoppingBasketIcon,
SoupKitchen as SoupKitchenIcon, Edit as EditIcon SoupKitchen as SoupKitchenIcon, Edit as EditIcon
} from '@mui/icons-material'; } from '@mui/icons-material';
import { ToBuy, Stuff, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, /*Stock*/ } from '../types/types'; import { ToBuy, Stuff, NewToBuy, NewStock, StuffAndCategoryAndAmount, StuffNameAndAmount, StockHistory, /*Stock*/ } from '../types/types';
import { TOBUY_ERRORS } from '../constants/errorMessages'; import { GENERAL_ERRORS, TOBUY_ERRORS } from '../constants/errorMessages';
import EditAmountDialog from '../components/EditAmountDialog'; import EditAmountDialog from '../components/EditAmountDialog';
import AddStuffAmountDialog from '../components/AddStuffAmountDialog'; import AddStuffAmountDialog from '../components/AddStuffAmountDialog';
import BuyDialog from '../components/BuyDialog'; import BuyDialog from '../components/BuyDialog';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import { useMessage } from '../components/MessageContext'; import { useMessage } from '../components/MessageContext';
import StuffHistoryDialog from '../components/StuffHistoryDialog';
//import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留 //import { FaCarrot } from "react-icons/fa6"; //エラー起きる いったん保留
@ -63,9 +64,11 @@ const TaskListPage: React.FC = () => {
const [openAmountDialog, setOpenAmountDialog] = useState(false); const [openAmountDialog, setOpenAmountDialog] = useState(false);
const [selectedToBuyId, setSelectedToBuyId] = useState<ToBuy["tobuyId"]>(0); const [selectedToBuyId, setSelectedToBuyId] = useState<ToBuy["tobuyId"]>(0);
const [newStock, setNewStock] = useState<NewStock>({ const [newStock, setNewStock] = useState<NewStock>({
amount: '', // 購入数量(ここではstring)
price: '', // ここではstring price: '', // ここではstring
shop: '',
buyDate: new Date().toISOString(), buyDate: new Date().toISOString(),
expDate: '', expDate: '',
}); });
@ -75,6 +78,7 @@ const TaskListPage: React.FC = () => {
const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0); const [selectedTask, setSelectedTask] = useState<ToBuy["tobuyId"]>(0);
// 編集対象の項目
const [editingItem, setEditingItem] = useState<ToBuy>({ const [editingItem, setEditingItem] = useState<ToBuy>({
tobuyId: 0, tobuyId: 0,
stuffId: 0, stuffId: 0,
@ -150,7 +154,7 @@ const TaskListPage: React.FC = () => {
const handleAddNewToBuy = async () => { const handleAddNewToBuy = async () => {
try { try {
if (isNaN(newToBuy.amount)) { if (isNaN(newToBuy.amount)) {
showErrorMessage('数量が正しくありません.'); showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return; return;
} }
@ -169,10 +173,10 @@ const TaskListPage: React.FC = () => {
* APIに送信して変更 * APIに送信して変更
* *
*/ */
const handleUpdateNewToBuy = async () => { const handleUpdateNewToBuy = async () => {
try { try {
if (isNaN(editingItem.amount)) { if (isNaN(editingItem.amount)) {
showErrorMessage('数量が正しくありません.'); showErrorMessage(GENERAL_ERRORS.INVALID_AMOUNT);
return; return;
} }
@ -196,11 +200,22 @@ const TaskListPage: React.FC = () => {
console.log("newPrice:", newStock.price) console.log("newPrice:", newStock.price)
console.log("parsedPrice: ", parsedPrice) console.log("parsedPrice: ", parsedPrice)
if (isNaN(parsedPrice)) { if (isNaN(parsedPrice)) {
showErrorMessage('価格が正しく入力されていません。') showErrorMessage(GENERAL_ERRORS.INVALID_PRICE)
return return
//setNewStock({ ...newStock, price: parsedPrice }); //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); setOpenInfoDialog(false);
fetchTasks(); // 変更後後の買うもの一覧を再取得 fetchTasks(); // 変更後後の買うもの一覧を再取得
} catch (error) { } 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 ( return (
<Container> <Container>
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
@ -230,6 +262,7 @@ const TaskListPage: React.FC = () => {
{/* 食材名 */} {/* 食材名 */}
<ListItemText <ListItemText
primary={tobuy.stuffName} primary={tobuy.stuffName}
onClick={() => handleShowHistories(tobuy)}
/> />
{/* 買い物リスト:食材情報記入ボタン */} {/* 買い物リスト:食材情報記入ボタン */}
<ListItemSecondaryAction> <ListItemSecondaryAction>
@ -239,7 +272,7 @@ const TaskListPage: React.FC = () => {
{/* 買い物リスト:数量変更ボタン */} {/* 買い物リスト:数量変更ボタン */}
<Tooltip title="数量変更"> <Tooltip title="数量変更">
<IconButton sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="数量変更" <IconButton sx={{ marginRight: 0, marginLeft: 0 }} edge="end" aria-label="数量変更"
onClick={() => { onClick={() => {
setOpenAmountDialog(true) setOpenAmountDialog(true)
setEditingItem(tobuy) setEditingItem(tobuy)
}} }}
@ -253,6 +286,7 @@ const TaskListPage: React.FC = () => {
onClick={() => { onClick={() => {
setOpenInfoDialog(true) setOpenInfoDialog(true)
setEditingItem(tobuy) setEditingItem(tobuy)
setNewStock({ ...newStock, amount: String(tobuy.amount) });
setSelectedToBuyId(tobuy.tobuyId) setSelectedToBuyId(tobuy.tobuyId)
// handleDeleteTask(tobuy.tobuyId) // handleDeleteTask(tobuy.tobuyId)
}}> }}>
@ -283,7 +317,7 @@ const TaskListPage: React.FC = () => {
setSelectedDeleteTask(tobuy) setSelectedDeleteTask(tobuy)
setOpenDeleteDialog(true) setOpenDeleteDialog(true)
} }
} }
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@ -337,7 +371,7 @@ const TaskListPage: React.FC = () => {
{/* 数量変更ダイアログ */} {/* 数量変更ダイアログ */}
<EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog} <EditAmountDialog openDialog={openAmountDialog} setOpenDialog={setOpenAmountDialog}
editingItem={editingItem} editingItem={editingItem}
setEditingItem={(v) => setEditingItem({...editingItem, ...v})} setEditingItem={(v) => setEditingItem({ ...editingItem, ...v })}
onSubmit={handleUpdateNewToBuy} /> onSubmit={handleUpdateNewToBuy} />
{/* 削除ダイアログ */} {/* 削除ダイアログ */}
@ -352,7 +386,7 @@ const TaskListPage: React.FC = () => {
<> <>
<Typography variant="h4">{selectedDeleteTask.stuffName}</Typography> <Typography variant="h4">{selectedDeleteTask.stuffName}</Typography>
<Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography> <Typography variant="body1" color="error"> 注意: 削除すると復元できません</Typography>
<Button onClick={() => setOpenDeleteDialog(false)} sx={{ mt: 3, mb: 2, left: '70%' }}></Button> <Button onClick={() => setOpenDeleteDialog(false)} sx={{ mt: 3, mb: 2, left: '70%' }}></Button>
<Button variant="contained" color="error" onClick={() => { <Button variant="contained" color="error" onClick={() => {
handleDeleteToBuy(selectedDeleteTask.tobuyId); handleDeleteToBuy(selectedDeleteTask.tobuyId);
setOpenDeleteDialog(false); // 削除処理後にダイアログを閉じる setOpenDeleteDialog(false); // 削除処理後にダイアログを閉じる
@ -362,6 +396,13 @@ const TaskListPage: React.FC = () => {
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* 履歴表示ダイアログ */}
{
(historyTobuy !== null && stockHistories !== null) &&
<StuffHistoryDialog openDialog={openHistoryDialog} setOpenDialog={setOpenHistoryDialog} stuffName={historyTobuy.stuffName} stockHistories={stockHistories} />
}
</Container> </Container>
); );

@ -3,7 +3,7 @@
* APIとの通信を担当するモジュール * 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'; import { AUTH_ERRORS, TOBUY_ERRORS, STOCK_ERRORS, RECIPE_ERRORS } from '../constants/errorMessages';
// APIのベースURL - 環境変数から取得するか、デフォルト値を使用 // 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.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) || ''
@ -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) // console.log('req: ', req)
req.buyDate = makeDateObject(req.buyDate)?.toISOString()?.substring(0, 10) || '' 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機能を提供するオブジェクト * API機能を提供するオブジェクト

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

Loading…
Cancel
Save