Skip to content
This repository was archived by the owner on Feb 9, 2026. It is now read-only.

Commit e30eb0f

Browse files
authored
Merge pull request #136 from u-hossy/feature/add-memo
frontend:メモ機能の追加
2 parents d3476ed + 03854da commit e30eb0f

File tree

9 files changed

+198
-10
lines changed

9 files changed

+198
-10
lines changed

frontend/public/notebook-text.svg

Lines changed: 1 addition & 0 deletions
Loading

frontend/src/components/BillingDetailCard.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@/components/ui/select";
1212
import type { Member } from "@/types/member";
1313
import type { Payment } from "@/types/payment";
14+
import { DialogMemo } from "./DialogMemo";
1415
import { Button } from "./ui/button";
1516

1617
interface BillingDetailCardProps {
@@ -27,8 +28,8 @@ export default function BillingDetailCard({
2728
setPayments,
2829
}: BillingDetailCardProps) {
2930
const [details, setDetails] = useState<
30-
Array<{ id: number; paidFor: number; amount: number | "" }>
31-
>([{ id: -1, paidFor: -1, amount: "" }]);
31+
Array<{ id: number; paidFor: number; amount: number | ""; memo: string }>
32+
>([{ id: -1, paidFor: -1, amount: "", memo: "" }]);
3233
const { eventId } = useParams();
3334

3435
// payerに関連するpaymentsをdetailsに反映
@@ -40,8 +41,9 @@ export default function BillingDetailCard({
4041
id: p.id,
4142
paidFor: p.paidFor,
4243
amount: p.amount,
44+
memo: p.memo,
4345
})),
44-
{ id: -1, paidFor: -1, amount: "" as number | "" },
46+
{ id: -1, paidFor: -1, amount: "" as number | "", memo: "" },
4547
];
4648

4749
setDetails(newDetails);
@@ -66,6 +68,7 @@ export default function BillingDetailCard({
6668
paidBy: paidBy.id,
6769
paidFor: newValue,
6870
amount: "",
71+
memo: "",
6972
};
7073
setPayments((prev) => [...prev, newPayment]);
7174

@@ -103,7 +106,7 @@ export default function BillingDetailCard({
103106

104107
const last = updated[updated.length - 1];
105108
if (last.amount !== "" && last.paidFor !== -1) {
106-
updated.push({ id: -1, paidFor: -1, amount: "" });
109+
updated.push({ id: -1, paidFor: -1, amount: "", memo: "" });
107110
}
108111

109112
return updated;
@@ -137,7 +140,10 @@ export default function BillingDetailCard({
137140
index === details.length - 1 &&
138141
(detail.paidFor !== -1 || detail.amount !== "")
139142
) {
140-
setDetails((prev) => [...prev, { id: -1, paidFor: -1, amount: "" }]);
143+
setDetails((prev) => [
144+
...prev,
145+
{ id: -1, paidFor: -1, amount: "", memo: "" },
146+
]);
141147
}
142148
};
143149

@@ -210,6 +216,13 @@ export default function BillingDetailCard({
210216
onBlur={() => handleBlur(index)}
211217
/>
212218
<span>円もらう</span>
219+
<DialogMemo
220+
index={index}
221+
disabled={!isSelectable}
222+
details={details}
223+
setDetails={setDetails}
224+
setPayments={setPayments}
225+
/>
213226
<Button
214227
variant="destructive"
215228
onClick={() => handleDeleteBilling(index)}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { DialogDescription } from "@radix-ui/react-dialog";
2+
import { useParams } from "react-router-dom";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogHeader,
7+
DialogTitle,
8+
DialogTrigger,
9+
} from "@/components/ui/dialog";
10+
import { Input } from "@/components/ui/input";
11+
import type { Payment } from "@/types/payment";
12+
13+
interface DialogMemoProps {
14+
index: number;
15+
disabled: boolean;
16+
details: Array<{
17+
id: number;
18+
paidFor: number;
19+
amount: number | "";
20+
memo: string;
21+
}>;
22+
setDetails: React.Dispatch<
23+
React.SetStateAction<
24+
Array<{ id: number; paidFor: number; amount: number | ""; memo: string }>
25+
>
26+
>;
27+
setPayments: React.Dispatch<React.SetStateAction<Payment[]>>;
28+
}
29+
30+
export function DialogMemo({
31+
index,
32+
disabled,
33+
details,
34+
setDetails,
35+
setPayments,
36+
}: DialogMemoProps) {
37+
const { eventId } = useParams();
38+
const handleMemoChange = (index: number, v: string) => {
39+
setDetails((prev) => {
40+
const updated = [...prev];
41+
updated[index].memo = v;
42+
43+
const last = updated[updated.length - 1];
44+
if (last.amount !== "" && last.paidFor !== -1) {
45+
updated.push({ id: -1, paidFor: -1, amount: "", memo: "" });
46+
}
47+
48+
return updated;
49+
});
50+
};
51+
52+
const handleMemoBlur = async (index: number) => {
53+
const detail = details[index];
54+
55+
if (detail.id !== -1) {
56+
// 既存を更新
57+
setPayments((prev) =>
58+
prev.map((p) =>
59+
p.id === detail.id
60+
? { ...p, amount: Number(detail.amount) || 0, memo: detail.memo }
61+
: p,
62+
),
63+
);
64+
}
65+
66+
await fetch(`http://127.0.0.1:8000/api/v1/payments/patch_by_key/`, {
67+
method: "PATCH",
68+
headers: { "Content-Type": "application/json" },
69+
body: JSON.stringify({
70+
event_id: eventId,
71+
payment_id: detail.id,
72+
note: detail.memo,
73+
}),
74+
});
75+
76+
if (
77+
index === details.length - 1 &&
78+
(detail.paidFor !== -1 || detail.amount !== "")
79+
) {
80+
setDetails((prev) => [
81+
...prev,
82+
{ id: -1, paidFor: -1, amount: "", memo: "" },
83+
]);
84+
}
85+
};
86+
return (
87+
<Dialog>
88+
<DialogTrigger asChild>
89+
<div
90+
className={`cursor-pointer select-none rounded-full p-2 transition ${disabled ? "pointer-events-none opacity-40" : "hover:bg-gray-200"}
91+
`}
92+
>
93+
<img
94+
src="/notebook-text.svg"
95+
alt="ノートアイコン"
96+
className="mr-2 ml-2 h-9 w-9"
97+
/>
98+
</div>
99+
</DialogTrigger>
100+
<DialogContent className="sm:max-w-md">
101+
<DialogHeader>
102+
<DialogTitle>メモ</DialogTitle>
103+
<DialogDescription>
104+
入力後に閉じると自動で保存されます。
105+
</DialogDescription>
106+
</DialogHeader>
107+
<div className="flex items-center gap-2">
108+
<div className="grid flex-1 gap-2">
109+
<Input
110+
type="string"
111+
placeholder=""
112+
value={details[index].memo}
113+
onChange={(e) => handleMemoChange(index, e.target.value)}
114+
onBlur={() => handleMemoBlur(index)}
115+
/>
116+
</div>
117+
</div>
118+
</DialogContent>
119+
</Dialog>
120+
);
121+
}

frontend/src/components/ExampleBillingDetailCard.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "@/components/ui/select";
1010
import { sampleMembers, samplePayments } from "@/data/sampleData";
1111
import type { Member } from "@/types/member";
12+
import { ExampleDialogMemo } from "./ExampleDialogMemo";
1213
import { Button } from "./ui/button";
1314

1415
interface ExampleBillingDetailCardProps {
@@ -36,7 +37,9 @@ export default function ExampleBillingDetailCard({
3637
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
3738
<div className="flex items-center gap-2">
3839
<Select
39-
value={detail.paidFor === -1 ? "" : String(detail.paidFor)}
40+
defaultValue={
41+
detail.paidFor === -1 ? "" : String(detail.paidFor)
42+
}
4043
>
4144
<SelectTrigger className="w-32">
4245
<SelectValue placeholder="請求先" />
@@ -59,9 +62,10 @@ export default function ExampleBillingDetailCard({
5962
disabled={!isSelectable}
6063
placeholder="金額"
6164
className="w-24"
62-
value={detail.amount}
65+
defaultValue={detail.amount}
6366
/>
6467
<span>円もらう</span>
68+
<ExampleDialogMemo />{" "}
6569
<Button variant="destructive" className="cursor-pointer">
6670
削除
6771
</Button>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { DialogDescription } from "@radix-ui/react-dialog";
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogHeader,
6+
DialogTitle,
7+
DialogTrigger,
8+
} from "@/components/ui/dialog";
9+
import { Input } from "@/components/ui/input";
10+
11+
export function ExampleDialogMemo() {
12+
return (
13+
<Dialog>
14+
<DialogTrigger asChild>
15+
<div
16+
className={`cursor-pointer select-none rounded-full p-2 transition`}
17+
>
18+
<img
19+
src="/notebook-text.svg"
20+
alt="ノートアイコン"
21+
className="mr-2 ml-2 h-9 w-9"
22+
/>
23+
</div>
24+
</DialogTrigger>
25+
<DialogContent className="sm:max-w-md">
26+
<DialogHeader>
27+
<DialogTitle>メモ</DialogTitle>
28+
<DialogDescription>
29+
入力後に閉じると自動で保存されます。
30+
</DialogDescription>
31+
</DialogHeader>
32+
<div className="flex items-center gap-2">
33+
<div className="grid flex-1 gap-2">
34+
<Input
35+
type="string"
36+
placeholder=""
37+
defaultValue="請求に関する簡単なメモを残すことができます。"
38+
/>
39+
</div>
40+
</div>
41+
</DialogContent>
42+
</Dialog>
43+
);
44+
}

frontend/src/pages/BillingPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface PaymentResponse {
2323
paid_by: number;
2424
paid_for: number;
2525
amount: number;
26+
note: string;
2627
}
2728

2829
export default function BillingPage({
@@ -44,6 +45,7 @@ export default function BillingPage({
4445
paidBy: p.paid_by,
4546
paidFor: p.paid_for,
4647
amount: p.amount,
48+
memo: p.note,
4749
})),
4850
),
4951
);

frontend/src/pages/TopPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function TopPage() {
4545
<Input
4646
//各inputを登録
4747
type="text"
48-
value={member.name}
48+
defaultValue={member.name}
4949
placeholder={`メンバー${index + 1}`}
5050
className="rounded-md border px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-400"
5151
/>
@@ -116,7 +116,7 @@ export default function TopPage() {
116116
</div>
117117
</CardHeader>
118118
<CardContent>
119-
各メンバーに請求したい金額を入力してください。「次へ」を押すと計算結果画面に、「戻る」を押すとメンバー追加画面に移動します。
119+
各メンバーに請求したい金額を入力してください。ノートアイコンを押すことで請求に関する簡単なメモを残すことができます。「次へ」を押すと計算結果画面に、「戻る」を押すとメンバー追加画面に移動します。
120120
</CardContent>
121121
</Card>
122122
</div>
@@ -132,7 +132,7 @@ export default function TopPage() {
132132
<label htmlFor={useId()} className="font-medium text-sm">
133133
計算アルゴリズム
134134
</label>
135-
<Select value={""}>
135+
<Select>
136136
<SelectTrigger id={useId()}>
137137
<SelectValue placeholder="アルゴリズムを選択してください" />
138138
</SelectTrigger>

frontend/src/types/payment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface Payment {
55
amount: number | "";
66
paidBy: MemberId;
77
paidFor: MemberId;
8+
memo: string;
89
}
910

1011
export type { Payment };

frontend/test/lib/caseConvert.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ test("convert snake case object to camel case object", () => {
99
amount: 8000,
1010
paid_by: 0,
1111
paid_for: 1,
12+
memo: "",
1213
};
1314

1415
const after: Payment = {
1516
id: 0,
1617
amount: 8000,
1718
paidBy: 0,
1819
paidFor: 1,
20+
memo: "",
1921
};
2022

2123
expect(toCamelCaseObject(before)).toStrictEqual(after);

0 commit comments

Comments
 (0)