diff --git a/backend/src/main/java/com/example/todoapp/config/InitTables.java b/backend/src/main/java/com/example/todoapp/config/InitTables.java index 642fe97..c31af71 100644 --- a/backend/src/main/java/com/example/todoapp/config/InitTables.java +++ b/backend/src/main/java/com/example/todoapp/config/InitTables.java @@ -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 diff --git a/backend/src/main/java/com/example/todoapp/controller/StocksController.java b/backend/src/main/java/com/example/todoapp/controller/StocksController.java index aebe005..4ca9713 100644 --- a/backend/src/main/java/com/example/todoapp/controller/StocksController.java +++ b/backend/src/main/java/com/example/todoapp/controller/StocksController.java @@ -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 getStockById( + public ResponseEntity 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 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 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(); - - // ユーザー情報を取得 - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + Long stockId = request.getStockId(); + + // 認証されたユーザー名を取得,ユーザーの在庫かどうか確かめる + 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 response = new HashMap<>(); @@ -147,4 +159,25 @@ public class StocksController { return ResponseEntity.ok(response); } + + @GetMapping("/getHistory") + public ResponseEntity> getHistory( + Authentication authentication, + @RequestParam Long stuffId + ) { + + // ユーザー情報を取得 + String username = authentication.getName(); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + // 材料IDを指定して在庫情報を取得 + List stocks = stockService.getStockByStuffId(user.getId(), stuffId); + List stockHistories = stocks.stream() + .map(StockHistoryDTO::fromEntity) + .collect(Collectors.toList()); + + return ResponseEntity.ok(stockHistories); + } + } \ No newline at end of file diff --git a/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java b/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java index d5753e9..e263330 100644 --- a/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java +++ b/backend/src/main/java/com/example/todoapp/controller/ToBuysController.java @@ -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()); diff --git a/backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java b/backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java index b41778e..ae7fea3 100644 --- a/backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/AddStocksDTO.java @@ -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; diff --git a/backend/src/main/java/com/example/todoapp/dto/BuyRequestDTO.java b/backend/src/main/java/com/example/todoapp/dto/BuyRequestDTO.java index f0e2ba5..0681f8f 100644 --- a/backend/src/main/java/com/example/todoapp/dto/BuyRequestDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/BuyRequestDTO.java @@ -25,6 +25,18 @@ public class BuyRequestDTO { */ private int price; + /** + * 価格 + * 実際の購入数量を設定します + */ + private int amount; + + /** + * 店舗 + * 購入店舗を設定します + */ + private String shop; + /** * 消費期限 * 食材の消費期限を設定します diff --git a/backend/src/main/java/com/example/todoapp/dto/StockDTO.java b/backend/src/main/java/com/example/todoapp/dto/StockDTO.java index 284b134..88b2547 100644 --- a/backend/src/main/java/com/example/todoapp/dto/StockDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/StockDTO.java @@ -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()); diff --git a/backend/src/main/java/com/example/todoapp/dto/StockHistoryDTO.java b/backend/src/main/java/com/example/todoapp/dto/StockHistoryDTO.java new file mode 100644 index 0000000..f998315 --- /dev/null +++ b/backend/src/main/java/com/example/todoapp/dto/StockHistoryDTO.java @@ -0,0 +1,42 @@ +package com.example.todoapp.dto; + +import com.example.todoapp.model.Stocks; + +import lombok.Data; + +import java.time.LocalDate; + +/** + * 在庫のデータ転送オブジェクト(DTO) + *

+ * このクラスはクライアントとサーバー間で在庫情報をやり取りするために使用されます。 + * エンティティとは異なり、必要な情報のみを含み、関連エンティティへの参照ではなくIDのみを保持します。 + *

+ */ + +@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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/example/todoapp/dto/StockResponseDTO.java b/backend/src/main/java/com/example/todoapp/dto/StockResponseDTO.java index 5098319..2ac7a26 100644 --- a/backend/src/main/java/com/example/todoapp/dto/StockResponseDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/StockResponseDTO.java @@ -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()); diff --git a/backend/src/main/java/com/example/todoapp/dto/UpdateStockRequestDTO.java b/backend/src/main/java/com/example/todoapp/dto/UpdateStockRequestDTO.java index 3d3333a..d3e3c11 100644 --- a/backend/src/main/java/com/example/todoapp/dto/UpdateStockRequestDTO.java +++ b/backend/src/main/java/com/example/todoapp/dto/UpdateStockRequestDTO.java @@ -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; diff --git a/backend/src/main/java/com/example/todoapp/model/Stocks.java b/backend/src/main/java/com/example/todoapp/model/Stocks.java index acc35df..36ed9d9 100644 --- a/backend/src/main/java/com/example/todoapp/model/Stocks.java +++ b/backend/src/main/java/com/example/todoapp/model/Stocks.java @@ -40,17 +40,17 @@ public class Stocks { private Long stockId; - /** + /** * 商品テーブル参照用の外部キー */ - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn( - name = "stuffId", - referencedColumnName = "stuffId", - nullable = false - ) - private Stuffs stuff; + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + name = "stuffId", + referencedColumnName = "stuffId", + nullable = false + ) + private Stuffs stuff; /** * タスクの所有者(ユーザー) @@ -68,11 +68,23 @@ public class Stocks { private int amount = 1; /** - * シングルの値段(デフォルト値: 0) + * 購入時数量(オプション) + */ + @Column(nullable = true) + private Integer buyAmount; + + /** + * 購入価格(合計) */ @Column(nullable = false) private int price = 0; + /** + * 購入店舗(オプション) + */ + @Column(nullable = true) + private String shop; + /** * 購入日 */ diff --git a/backend/src/main/java/com/example/todoapp/model/ToBuys.java b/backend/src/main/java/com/example/todoapp/model/ToBuys.java index e33edfb..2ec617e 100644 --- a/backend/src/main/java/com/example/todoapp/model/ToBuys.java +++ b/backend/src/main/java/com/example/todoapp/model/ToBuys.java @@ -72,10 +72,10 @@ public class ToBuys { private int amount = 1; /** - * 購入するお店 + * 購入店舗 */ @Column(nullable = false) - private String store; + private String shop; } diff --git a/backend/src/main/java/com/example/todoapp/repository/StocksRepository.java b/backend/src/main/java/com/example/todoapp/repository/StocksRepository.java index 3a069d4..87d2cdd 100644 --- a/backend/src/main/java/com/example/todoapp/repository/StocksRepository.java +++ b/backend/src/main/java/com/example/todoapp/repository/StocksRepository.java @@ -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 { /** * userIdから在庫一覧をstockId順で取得する + * (数量が 0 のものは除く) * * @param userId 検索するユーザーID * @return 在庫リスト */ - List findStocksByUserIdOrderByStockIdAsc(Long userId); + @Query("SELECT s FROM Stocks s WHERE s.user.id = :userId AND s.amount <> 0 ORDER BY s.stockId DESC") + List findUserStocks(Long userId); /** * 在庫情報を更新する @@ -46,22 +51,47 @@ public interface StocksRepository extends JpaRepository { // 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 findStocksByUserIdAndStuff_StuffIdOrderByBuyDateDescStockIdDesc(Long userId, Long stuffId); + } \ No newline at end of file diff --git a/backend/src/main/java/com/example/todoapp/service/StocksService.java b/backend/src/main/java/com/example/todoapp/service/StocksService.java index cc45335..1be09d1 100644 --- a/backend/src/main/java/com/example/todoapp/service/StocksService.java +++ b/backend/src/main/java/com/example/todoapp/service/StocksService.java @@ -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 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 getStockByStuffId(Long userId, Long stuffId) { + return stocksRepository.findStocksByUserIdAndStuff_StuffIdOrderByBuyDateDescStockIdDesc(userId, stuffId); + } + /** * ユーザー名からユーザーエンティティを取得する * diff --git a/backend/src/main/java/com/example/todoapp/service/ToBuysService.java b/backend/src/main/java/com/example/todoapp/service/ToBuysService.java index fe3bfe7..2c0cc2a 100644 --- a/backend/src/main/java/com/example/todoapp/service/ToBuysService.java +++ b/backend/src/main/java/com/example/todoapp/service/ToBuysService.java @@ -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); diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index 8b9e0f2..adb4a07 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -12,4 +12,13 @@ spring: lob: non_contextual_creation: true cors: - allowed-origins: http://localhost:3000 \ No newline at end of file + 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 diff --git a/frontend/src/components/BuyDialog.tsx b/frontend/src/components/BuyDialog.tsx index 27485fe..622e3b3 100644 --- a/frontend/src/components/BuyDialog.tsx +++ b/frontend/src/components/BuyDialog.tsx @@ -1,12 +1,12 @@ import { useState } from 'react'; import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, - Button, - Box, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Button, + Box, } from '@mui/material'; import { NewStock } from '../types/types'; import DatePicker, { registerLocale } from 'react-datepicker'; @@ -21,78 +21,95 @@ const formatDateLocal = (date: Date) => { }; // 日本語ロケールを登録 - registerLocale('ja', ja); +registerLocale('ja', ja); const BuyDialog = ({ - openDialog, - setOpenDialog, - stuffName, - newStock, - setNewStock, - onSubmit, + openDialog, + setOpenDialog, + stuffName, + newStock, + setNewStock, + onSubmit, }: { - openDialog: boolean, - setOpenDialog: (open: boolean) => void, - stuffName: string, - newStock: NewStock, - setNewStock: (tobuy: NewStock) => void, - onSubmit: () => void, + openDialog: boolean, + setOpenDialog: (open: boolean) => void, + stuffName: string, + newStock: NewStock, + setNewStock: (tobuy: NewStock) => void, + onSubmit: () => void, }) => { - - return ( + return ( - setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }} -> - 在庫登録 - - - {/* 材料名表示 */} - - {/* 価格入力フィールド */} - { - const value = e.target.value; - if (/^\d*$/.test(value)) { - setNewStock({ ...newStock, price: value }) - }; - }} - sx={{ marginBottom: 2 }} - /> - {/* 購入日・消費期限を横並びに */} - - {/* 購入日入力フィールド */} - - setNewStock({ ...newStock, buyDate: date ? formatDateLocal(date) : '' }) - } - dateFormat="yyyy/MM/dd" - customInput={ - - } - isClearable - //withPortal // ← 他の文字との重なり対策 - /> - {/* + setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minHeight: '600px', maxHeight: '80vh' } }} + > + 在庫登録 + + + {/* 材料名表示 */} + + + {/* 価格入力フィールド */} + { + const value = e.target.value; + if (/^\d*$/.test(value)) { + setNewStock({ ...newStock, price: value }) + }; + }} + sx={{ marginBottom: 2 }} + /> + + {/* 価格入力フィールド */} + { + const value = e.target.value; + if (/^\d*$/.test(value)) { + setNewStock({ ...newStock, amount: value }) + }; + }} + sx={{ marginBottom: 2 }} + /> + + {/* 購入日・消費期限を横並びに */} + + {/* 購入日入力フィールド */} + + setNewStock({ ...newStock, buyDate: date ? formatDateLocal(date) : '' }) + } + dateFormat="yyyy/MM/dd" + customInput={ + + } + isClearable + //withPortal // ← 他の文字との重なり対策 + /> + {/* setNewStock({ ...newStock, buyDate: e.target.value })} /> */} - {/* 消費・賞味期限入力フィールド */} - - setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' }) - } - dateFormat="yyyy/MM/dd" - customInput={ - - } - isClearable - //withPortal - /> - - {/* 購入店舗入力フィールド */} - {/* TODO: 実装 */} - - - - - - - - + {/* 消費・賞味期限入力フィールド */} + + setNewStock({ ...newStock, expDate: date ? formatDateLocal(date) : '' }) + } + dateFormat="yyyy/MM/dd" + customInput={ + + } + isClearable + //withPortal + /> + + {/* 購入店舗入力フィールド */} + setNewStock({...newStock, shop: e.target.value})} + fullWidth + /> + + + + + + + - ) + ) } export default BuyDialog; \ No newline at end of file diff --git a/frontend/src/components/StuffHistoryDialog.tsx b/frontend/src/components/StuffHistoryDialog.tsx new file mode 100644 index 0000000..db70a56 --- /dev/null +++ b/frontend/src/components/StuffHistoryDialog.tsx @@ -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 ( + setOpenDialog(false)} disableScrollLock={true} PaperProps={{ sx: { minWidth: '300px', maxHeight: '80vh' } }}> + + 購入履歴: {stuffName} + setOpenDialog(false)} + sx={{ + position: 'absolute', + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }} + > + + + + {/* dividersを追加して区切り線を表示, 表をぴったりくっつける */} + {(!stockHistories.length ? + 購入履歴はありません。 + : + + + + + {/* 各ヘッダーセルに white-space: nowrap; を適用 */} + 購入日 + {/* 「購入店舗」ヘッダーも nowrap にし、minWidth でスクロールを考慮 */} + 購入店舗 + 購入数量 + 購入価格 + + + + {stockHistories.map((history) => ( + + {/* 各ボディセルに white-space: nowrap; を適用 */} + + {history.buyDate} + + {/* 「購入店舗」セルに nowrap と minWidth を適用 */} + {history.shop || ''} + {history.buyAmount} + {history.price} + + ))} + +
+
+ )} +
+ {/* 必要であればDialogActionsにボタンなどを追加できます */} + {/* + + */} +
+ ); +} + +export default StuffHistoryDialog; \ No newline at end of file diff --git a/frontend/src/constants/errorMessages.ts b/frontend/src/constants/errorMessages.ts index 90bcb50..7dab57b 100644 --- a/frontend/src/constants/errorMessages.ts +++ b/frontend/src/constants/errorMessages.ts @@ -6,6 +6,8 @@ // 一般的なエラーメッセージ export const GENERAL_ERRORS = { UNEXPECTED_ERROR: '予期せぬエラーが発生しました', + INVALID_AMOUNT: '数量が正しく入力されていません。', + INVALID_PRICE: '価格が正しく入力されていません。', }; // 認証関連のエラーメッセージ diff --git a/frontend/src/pages/StockPage.tsx b/frontend/src/pages/StockPage.tsx index bdcde90..e2fd63d 100644 --- a/frontend/src/pages/StockPage.tsx +++ b/frontend/src/pages/StockPage.tsx @@ -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 & { 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(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; + } + + const lastUpdate = new Date().toISOString().substring(0, 10); + + const updateRequest = { + stockId, + amount, + buyAmount, + price, + shop, + buyDate, + expDate, + lastUpdate, + } + + console.log('updateRequest:', updateRequest); - await handleUpdateStock( - editStock.stockId, - Number(editStock.amount), - Number(editStock.price), - editStock.buyDate, - editStock.expDate - ); + 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 = () => { {editStock && ( <> + + {/* 材料名 */} {editStock.stuffName} + { e.preventDefault(); } }} + /> + { + if (e.key === '-' || e.key === 'e' || e.key === 'E') { + e.preventDefault(); + } + }} /> + { e.preventDefault(); } }} + /> + + {/* 購入日・消費期限を横並びに */} {/* 購入日 */} diff --git a/frontend/src/pages/TaskListPage.tsx b/frontend/src/pages/TaskListPage.tsx index fd2335f..89ac674 100644 --- a/frontend/src/pages/TaskListPage.tsx +++ b/frontend/src/pages/TaskListPage.tsx @@ -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"; //エラー起きる いったん保留 @@ -63,9 +64,11 @@ const TaskListPage: React.FC = () => { const [openAmountDialog, setOpenAmountDialog] = useState(false); const [selectedToBuyId, setSelectedToBuyId] = useState(0); - + const [newStock, setNewStock] = useState({ + amount: '', // 購入数量(ここではstring) price: '', // ここではstring + shop: '', buyDate: new Date().toISOString(), expDate: '', }); @@ -75,6 +78,7 @@ const TaskListPage: React.FC = () => { const [selectedTask, setSelectedTask] = useState(0); + // 編集対象の項目 const [editingItem, setEditingItem] = useState({ 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; } @@ -169,10 +173,10 @@ const TaskListPage: React.FC = () => { * 入力されたタスク情報をAPIに送信して変更 * 作成後はダイアログを閉じ、入力内容をリセット */ - const handleUpdateNewToBuy = async () => { + 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(null); + const [stockHistories, setStockHistories] = useState(null); + + const handleShowHistories = async (tobuy: ToBuy) => { + console.log('handleShowHistories:', tobuy); + try { + setHistoryTobuy(tobuy); + setStockHistories(await stockApi.getHistories(tobuy.stuffId)); + setOpenHistoryDialog(true); + } catch { + showErrorMessage("履歴の読み込みに失敗しました。"); + } + } + return ( @@ -230,6 +262,7 @@ const TaskListPage: React.FC = () => { {/* 食材名 */} handleShowHistories(tobuy)} /> {/* 買い物リスト:食材情報記入ボタン */} @@ -239,7 +272,7 @@ const TaskListPage: React.FC = () => { {/* 買い物リスト:数量変更ボタン */} { + onClick={() => { setOpenAmountDialog(true) setEditingItem(tobuy) }} @@ -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) }}> @@ -283,7 +317,7 @@ const TaskListPage: React.FC = () => { setSelectedDeleteTask(tobuy) setOpenDeleteDialog(true) } - } + } > @@ -337,7 +371,7 @@ const TaskListPage: React.FC = () => { {/* 数量変更ダイアログ */} setEditingItem({...editingItem, ...v})} + setEditingItem={(v) => setEditingItem({ ...editingItem, ...v })} onSubmit={handleUpdateNewToBuy} /> {/* 削除ダイアログ */} @@ -352,7 +386,7 @@ const TaskListPage: React.FC = () => { <> {selectedDeleteTask.stuffName}を削除します。 ⚠️ 注意: 削除すると復元できません。 - +