diff --git a/app/routes/admin/sponsor.tsx b/app/routes/admin/sponsor.tsx
index 85b5529..05f9d6a 100644
--- a/app/routes/admin/sponsor.tsx
+++ b/app/routes/admin/sponsor.tsx
@@ -7,6 +7,7 @@ import { Button } from "~/components/ui/button";
import { confirmSubmit, promptDialog } from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { Select } from "~/components/ui/select";
+import { Textarea } from "~/components/ui/textarea";
import { UserAvatar } from "~/components/user-avatar";
import { pageTitle } from "~/share-meta";
import { fromWallClock, unixNow, wallClock } from "~/time";
@@ -58,7 +59,8 @@ const DONE: Record = {
confirm: "已确认",
amount: "已修改金额",
reject: "已标为未收到",
- message: "已删除留言",
+ message: "已保存留言",
+ anonymous: "已修改",
delete: "已删除",
};
@@ -100,7 +102,9 @@ export async function action({ request, context }: Route.ActionArgs) {
? { amount: fen() }
: intent === "reject"
? { status: "rejected", reason: field("reason") }
- : { message: null },
+ : intent === "anonymous"
+ ? { anonymous: field("anonymous") === "on" }
+ : { message: field("message") },
});
if (res.ok) return { intent, ok: true, message: DONE[intent] ?? "已保存" };
return { intent, ok: false, message: (await res.json()).error.message };
@@ -213,20 +217,39 @@ function Row({ item: r }: { item: Item }) {
>
)}
- {r.message && (
- {r.message}
- )}
+ {/* Saved empty, the message is taken down. The boxes are keyed by what's saved, so a change
+ from anywhere replaces what they hold; they are off while one is sent, or what is typed
+ meanwhile would be replaced too. */}
+
{r.status === "rejected" && r.reason && (
理由:{r.reason}
)}
+
实收 ¥
@@ -250,18 +273,20 @@ function Row({ item: r }: { item: Item }) {
{confirmed ? "撤销确认" : "未收到"}
)}
- {r.message && (
-
- 删除留言
-
- )}
+
+ {r.anonymous ? "显示名字" : "改为匿名"}
+
{
expect(late.status).toBe(409);
});
+ test("收款人可以改墙上的留言,清空就删掉", async () => {
+ const someone = await member();
+ const id = await register(someone.who);
+ await review(id, { status: "confirmed" });
+ expect((await review(id, { message: " 祝大家都上岸 " })).status).toBe(204);
+ expect((await wall()).find((t) => t.id === id)).toMatchObject({ message: "祝大家都上岸" });
+ await review(id, { message: "" });
+ expect((await wall()).find((t) => t.id === id)).toMatchObject({ message: null });
+ });
+
+ test("收款人可以改是否匿名", async () => {
+ const someone = await member();
+ const id = await register(someone.who, 700, true);
+ await review(id, { status: "confirmed" });
+ expect((await wall()).find((t) => t.id === id)).toMatchObject({ user: null });
+ expect((await review(id, { anonymous: false })).status).toBe(204);
+ expect((await wall()).find((t) => t.id === id)).toMatchObject({ user: { id: someone.id } });
+ });
+
test("没收到的附理由通知本人,不上墙", async () => {
const someone = await member();
const id = await register(someone.who);
diff --git a/server/modules/sponsors/routes.ts b/server/modules/sponsors/routes.ts
index 8018f0f..5fcfd89 100644
--- a/server/modules/sponsors/routes.ts
+++ b/server/modules/sponsors/routes.ts
@@ -441,7 +441,10 @@ export const sponsorsRoutes = createRouter()
.transform((r) => r || null)
.openapi({ description: "标为没收到的理由" }),
amount: Amount.optional(),
- message: z.null().optional().openapi({ description: "为 null 时删掉留言" }),
+ anonymous: z.boolean().optional().openapi({ description: "感谢墙上是否匿名" }),
+ message: Message.optional().openapi({
+ description: `新的留言,最多 ${MAX_MESSAGE} 个字;为 null 或空时删掉留言`,
+ }),
}),
},
},
diff --git a/server/modules/sponsors/service.ts b/server/modules/sponsors/service.ts
index 2576cbd..351930a 100644
--- a/server/modules/sponsors/service.ts
+++ b/server/modules/sponsors/service.ts
@@ -149,8 +149,8 @@ async function lockOne(trx: Transaction, id: number) {
);
}
-// Whoever paid may go anonymous or not at any time, and take the message down; one shown on the
-// wall was read by the payee when confirming it, so it can't be changed afterwards.
+// Whoever paid may go anonymous or not at any time, and take the message down, but not reword it:
+// every message on the wall is one the payee has read, so corrections go through the payee.
export async function editOwnSponsorship(
actor: Actor,
id: number,
@@ -219,8 +219,8 @@ export async function addSponsorship(
});
}
-// Confirmed with what really came, turned down with why, a confirmation taken back, or a message
-// taken down. Whoever paid hears when its status changes.
+// Confirmed with what really came, turned down with why, a confirmation taken back, the message
+// corrected or taken down, or anonymity set right. Whoever paid hears when its status changes.
export async function reviewSponsorship(
actor: Actor,
id: number,
@@ -228,7 +228,8 @@ export async function reviewSponsorship(
status?: "confirmed" | "rejected";
reason?: string | null;
amount?: number;
- message?: null;
+ anonymous?: boolean;
+ message?: string | null;
},
) {
await db.transaction().execute(async (trx) => {
@@ -249,6 +250,7 @@ export async function reviewSponsorship(
.updateTable("sponsorships")
.set({
amount: after.amount,
+ anonymous: after.anonymous,
message: after.message,
status: after.status,
reason: after.reason,
@@ -273,7 +275,13 @@ export async function reviewSponsorship(
amount: after.amount,
...(rest.amount !== undefined &&
rest.amount !== before.amount && { amount_was: before.amount }),
- ...(rest.message === null && before.message !== null && { message_was: before.message }),
+ ...(rest.anonymous !== undefined &&
+ rest.anonymous !== before.anonymous && { anonymous: after.anonymous }),
+ ...(rest.message !== undefined &&
+ rest.message !== before.message && {
+ message: after.message,
+ message_was: before.message,
+ }),
...(reviewed && { status_was: before.status, reason: after.reason }),
},
});