diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8eef912..52caf6e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,5 +1,6 @@
import {
faGear,
+ faHistory,
faHouse,
faUserEdit,
IconDefinition,
@@ -11,7 +12,8 @@ import { ItemsProvider } from "contexts/ItemsContext";
import { Header } from "components/Header";
import { UserSetting } from "Setting/User/UserSetting";
import { AdminPane } from "Setting/Admin/AdminPane";
-import { Home } from "features/home/Home";
+import { Home } from "features/home";
+import { PurchaseHistory } from "features/purchase-history";
export const TabIndex = createContext(0);
@@ -34,6 +36,13 @@ function App() {
iconPosition: "left",
contents: ,
},
+ {
+ item: "history",
+ displayText: "購入履歴",
+ icon: faHistory,
+ iconPosition: "left",
+ contents: ,
+ },
{
item: "memberSettings",
displayText: "利用者設定",
diff --git a/frontend/src/features/home/components/MemberPane.tsx b/frontend/src/components/MemberPane.tsx
similarity index 95%
rename from frontend/src/features/home/components/MemberPane.tsx
rename to frontend/src/components/MemberPane.tsx
index a326722..42b2bfe 100644
--- a/frontend/src/features/home/components/MemberPane.tsx
+++ b/frontend/src/components/MemberPane.tsx
@@ -1,6 +1,6 @@
import { Heading, Stack } from "@chakra-ui/react";
import { useMembers } from "contexts/MembersContext";
-import { MemberCard } from "./MemberCard";
+import { MemberCard } from "features/home/components/MemberCard";
type Props = {
selectedMemberId: string;
diff --git a/frontend/src/components/TwoColumnLayout.tsx b/frontend/src/components/TwoColumnLayout.tsx
index 78ba4b5..bbd27a8 100644
--- a/frontend/src/components/TwoColumnLayout.tsx
+++ b/frontend/src/components/TwoColumnLayout.tsx
@@ -3,7 +3,7 @@ import { ReactNode } from "react";
type Props = {
children: ReactNode;
- h: string;
+ h?: string;
};
export function TwoColumnLayout({ children, h = "100%" }: Props) {
diff --git a/frontend/src/contexts/MembersContext.tsx b/frontend/src/contexts/MembersContext.tsx
index 9610ef8..d2adf99 100644
--- a/frontend/src/contexts/MembersContext.tsx
+++ b/frontend/src/contexts/MembersContext.tsx
@@ -59,7 +59,8 @@ type Action =
| { type: "added"; id: string; name: string; attribute: string }
| { type: "deleted"; id: string }
| { type: "switchedActivity"; id: string; active: boolean }
- | { type: "purchased"; id: string; price: number };
+ | { type: "purchased"; id: string; price: number }
+ | { type: "purchaseCanceled"; id: string; price: number };
function membersReducer(members: Member[], action: Action) {
switch (action.type) {
@@ -98,6 +99,16 @@ function membersReducer(members: Member[], action: Action) {
}
return member;
});
+ case "purchaseCanceled":
+ return members.map((member) => {
+ if (member.id === action.id) {
+ return {
+ ...member,
+ payment: Number(member.payment) - Number(action.price), // TODO: 明示的にNumberに変換しないとなぜかNaNになる
+ };
+ }
+ return member;
+ });
default:
throw new Error("invalid action");
}
diff --git a/frontend/src/features/home/Home.tsx b/frontend/src/features/home/index.tsx
similarity index 95%
rename from frontend/src/features/home/Home.tsx
rename to frontend/src/features/home/index.tsx
index d93df1a..0592090 100644
--- a/frontend/src/features/home/Home.tsx
+++ b/frontend/src/features/home/index.tsx
@@ -5,7 +5,7 @@ import {
RightColumn,
} from "components/TwoColumnLayout";
import { ItemPane } from "./components/ItemPane";
-import { MemberPane } from "./components/MemberPane";
+import { MemberPane } from "components/MemberPane";
import { ConfrimPane } from "./components/ConfirmPane";
function Home() {
diff --git a/frontend/src/features/purchase-history/components/HistoryCard.tsx b/frontend/src/features/purchase-history/components/HistoryCard.tsx
new file mode 100644
index 0000000..251ab04
--- /dev/null
+++ b/frontend/src/features/purchase-history/components/HistoryCard.tsx
@@ -0,0 +1,117 @@
+import {
+ AspectRatio,
+ Box,
+ Flex,
+ HStack,
+ IconButton,
+ Image,
+ Spacer,
+ Text,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { History } from "types";
+import { DateFormatter } from "util/DateFormatter";
+import { Backend } from "util/Backend";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import { useEffect, useState } from "react";
+import LogoDefaultItem from "image/default_item.svg";
+import { HistoryDeleteConfirm } from "./HistoryDeleteConfirm";
+
+type Props = {
+ history: History;
+ onDeleteHistory: (history: History) => void;
+};
+
+function HistoryCard({ history, onDeleteHistory: deleteHistory }: Props) {
+ // TODO: 画像処理の共通化
+ const [imageURL, setImageURL] = useState("");
+ const { isOpen, onClose, onOpen } = useDisclosure();
+
+ useEffect(() => {
+ let ignore = false;
+
+ async function getImage() {
+ if (history === null) {
+ setImageURL(LogoDefaultItem);
+ return;
+ }
+
+ const res = await Backend.getItemImage(history.itemId);
+ if (!ignore) {
+ if (res === null) {
+ console.log(
+ history.itemId + ": Custom image not found. So use default image."
+ );
+ setImageURL(LogoDefaultItem);
+ return;
+ }
+ setImageURL(URL.createObjectURL(res));
+ }
+ }
+ getImage();
+
+ return () => {
+ ignore = true;
+ };
+ }, []);
+
+ return (
+ <>
+
+
+ {history.itemId === null ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {history.itemName}
+
+
+
+ {DateFormatter.convertDateFormat(history.date) +
+ ", " +
+ history.price +
+ " 円"}
+
+
+
+ }
+ />
+
+ {
+ deleteHistory(history);
+ onClose();
+ }}
+ />
+ >
+ );
+}
+
+export { HistoryCard };
diff --git a/frontend/src/features/purchase-history/components/HistoryDeleteConfirm.tsx b/frontend/src/features/purchase-history/components/HistoryDeleteConfirm.tsx
new file mode 100644
index 0000000..090a126
--- /dev/null
+++ b/frontend/src/features/purchase-history/components/HistoryDeleteConfirm.tsx
@@ -0,0 +1,42 @@
+import {
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+} from "@chakra-ui/react";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ historyId: number;
+ onClickDeleteButton: () => void;
+};
+
+function HistoryDeleteConfirm({ isOpen, onClose, onClickDeleteButton }: Props) {
+ return (
+
+
+
+ 購入履歴の削除
+
+
+ 購入履歴を削除しますか?
+
+
+
+
+
+
+
+ );
+}
+
+export { HistoryDeleteConfirm };
diff --git a/frontend/src/features/purchase-history/components/HistoryList.tsx b/frontend/src/features/purchase-history/components/HistoryList.tsx
new file mode 100644
index 0000000..db9730a
--- /dev/null
+++ b/frontend/src/features/purchase-history/components/HistoryList.tsx
@@ -0,0 +1,45 @@
+import { Center, Heading, Text } from "@chakra-ui/react";
+import { History } from "types";
+import { HistoryCard } from "./HistoryCard";
+
+type Props = {
+ histories: History[];
+ isMemberSelected: boolean;
+ onDeleteHistory: (history: History) => void;
+};
+
+function HistoryList({ histories, isMemberSelected, onDeleteHistory }: Props) {
+ if (isMemberSelected === false) {
+ return (
+
+
+ 利用者を選択してください
+
+
+ );
+ }
+
+ if (histories.length === 0) {
+ return (
+
+
+ 購入履歴がありません
+
+
+ );
+ }
+
+ return (
+ <>
+ {histories.map((history) => (
+
+ ))}
+ >
+ );
+}
+
+export { HistoryList };
diff --git a/frontend/src/features/purchase-history/index.tsx b/frontend/src/features/purchase-history/index.tsx
new file mode 100644
index 0000000..996d610
--- /dev/null
+++ b/frontend/src/features/purchase-history/index.tsx
@@ -0,0 +1,86 @@
+import { useState } from "react";
+import { Heading, useToast } from "@chakra-ui/react";
+import {
+ LeftColumn,
+ RightColumn,
+ TwoColumnLayout,
+} from "components/TwoColumnLayout";
+import { MemberPane } from "components/MemberPane";
+import { HistoryList } from "./components/HistoryList";
+import { Backend } from "util/Backend";
+import { History } from "types";
+import { useMembersDispatch } from "contexts/MembersContext";
+
+function PurchaseHistory() {
+ const [selectedMemberId, setSelectedMemberId] = useState("");
+ const [histories, setHistories] = useState([]);
+ const dispatchMembers = useMembersDispatch();
+ const toast = useToast();
+
+ async function fetchHistories(memberId: string) {
+ const histories = await Backend.getUserHistory(memberId);
+ if (histories === null) {
+ console.error("getUserHistory: failed");
+ return;
+ }
+ setHistories(histories);
+ }
+
+ async function deleteHistory(history: History) {
+ if (!(await Backend.recall(history.historyId))) {
+ console.error("recall: failed");
+ toast({
+ title: "購入履歴の削除に失敗しました",
+ status: "error",
+ duration: 2000,
+ isClosable: true,
+ });
+ return;
+ }
+
+ // memberの利用金額を更新
+ dispatchMembers({
+ type: "purchaseCanceled",
+ id: history.memberId,
+ price: history.price,
+ });
+
+ // historyを更新
+ setHistories(histories.filter((h) => h.historyId !== history.historyId));
+
+ // toastを表示
+ toast({
+ title: "購入履歴を削除しました",
+ status: "success",
+ duration: 2000,
+ isClosable: true,
+ });
+ }
+
+ return (
+
+
+ {
+ // 購入履歴の取得が完了するまでmemberIdをセットしない
+ await fetchHistories(memberId);
+ setSelectedMemberId(memberId);
+ }}
+ />
+
+
+
+ 購入履歴
+
+
+
+
+ );
+}
+
+export { PurchaseHistory };