diff --git a/apps/web/app/dashboard/users/page.tsx b/apps/web/app/dashboard/users/page.tsx index 99412ce..39257a9 100644 --- a/apps/web/app/dashboard/users/page.tsx +++ b/apps/web/app/dashboard/users/page.tsx @@ -2,19 +2,23 @@ import { useEffect, useState } from "react"; import { + Autocomplete, Button, Chip, + EmptyState, Input, Label, ListBox, Modal, + SearchField, Select, Spinner, Table, TextField, + useFilter, } from "@heroui/react"; -import { Pencil } from "@gravity-ui/icons"; -import { api, type UserRow } from "@/lib/api"; +import { CreditCard, Pencil } from "@gravity-ui/icons"; +import { api, type IdTag, type UserRow } from "@/lib/api"; import { useSession } from "@/lib/auth-client"; type CreateForm = { @@ -38,6 +42,98 @@ const ROLE_OPTIONS = [ { key: "admin", label: "管理员" }, ]; +const statusColorMap: Record = { + Accepted: "success", + Blocked: "danger", + Expired: "warning", + Invalid: "danger", + ConcurrentTx: "warning", +}; + +function generateIdTag(): string { + const chars = "0123456789ABCDEF"; + let result = ""; + for (let i = 0; i < 8; i++) { + result += chars[Math.floor(Math.random() * chars.length)]; + } + return result; +} + +function fenToYuan(fen: number): string { + return (fen / 100).toFixed(2); +} + +function FreeTagBinder({ + freeTags, + selected, + onSelect, + onBind, + binding, +}: { + freeTags: IdTag[]; + selected: string; + onSelect: (v: string) => void; + onBind: () => void; + binding: boolean; +}) { + const { contains } = useFilter({ sensitivity: "base" }); + return ( +
+
+ + onSelect(key ? String(key) : "")} + > + + + {({ isPlaceholder, state }: any) => { + if (isPlaceholder || !state.selectedItems?.length) + return ( + + {freeTags.length === 0 ? "暂无无主卡" : "选择卡号"} + + ); + const tag = freeTags.find((t) => t.idTag === state.selectedItems[0]?.key); + return tag ? {tag.idTag} : null; + }} + + + + + + + + + + + + + + 无匹配卡号}> + {freeTags.map((t) => ( + + {t.idTag} + ¥{(t.balance / 100).toFixed(2)} + + + ))} + + + + +
+ +
+ ); +} + export default function UsersPage() { const { data: session } = useSession(); const currentUserId = session?.user?.id; @@ -50,6 +146,16 @@ export default function UsersPage() { const [editTarget, setEditTarget] = useState(null); const [editForm, setEditForm] = useState({ name: "", username: "", role: "user" }); const [saving, setSaving] = useState(false); + const [cardsUser, setCardsUser] = useState(null); + const [userTags, setUserTags] = useState([]); + const [tagsLoading, setTagsLoading] = useState(false); + const [newTagId, setNewTagId] = useState(""); + const [creatingTag, setCreatingTag] = useState(false); + const [unlinkingTag, setUnlinkingTag] = useState(null); + const [deletingTag, setDeletingTag] = useState(null); + const [freeTags, setFreeTags] = useState([]); + const [selectedFreeTag, setSelectedFreeTag] = useState(""); + const [bindingTag, setBindingTag] = useState(false); const load = async () => { setLoading(true); @@ -116,6 +222,71 @@ export default function UsersPage() { } }; + const loadUserTags = async (userId: string) => { + setTagsLoading(true); + try { + const all = await api.idTags.list(); + setUserTags(all.filter((t) => t.userId === userId)); + setFreeTags(all.filter((t) => !t.userId)); + } finally { + setTagsLoading(false); + } + }; + + const openCardsModal = (u: UserRow) => { + setCardsUser(u); + setSelectedFreeTag(""); + setUserTags([]); + setFreeTags([]); + void loadUserTags(u.id); + }; + + const handleCreateTag = async () => { + if (!cardsUser || !newTagId) return; + setCreatingTag(true); + try { + await api.idTags.create({ idTag: newTagId, userId: cardsUser.id }); + setNewTagId(generateIdTag()); + await loadUserTags(cardsUser.id); + } finally { + setCreatingTag(false); + } + }; + + const handleUnlinkTag = async (idTag: string) => { + if (!cardsUser) return; + setUnlinkingTag(idTag); + try { + await api.idTags.update(idTag, { userId: null }); + await loadUserTags(cardsUser.id); + } finally { + setUnlinkingTag(null); + } + }; + + const handleBindFreeTag = async () => { + if (!cardsUser || !selectedFreeTag) return; + setBindingTag(true); + try { + await api.idTags.update(selectedFreeTag, { userId: cardsUser.id }); + setSelectedFreeTag(""); + await loadUserTags(cardsUser.id); + } finally { + setBindingTag(false); + } + }; + + const handleDeleteTag = async (idTag: string) => { + if (!cardsUser) return; + setDeletingTag(idTag); + try { + await api.idTags.delete(idTag); + await loadUserTags(cardsUser.id); + } finally { + setDeletingTag(null); + } + }; + const isEditingSelf = editTarget?.id === currentUserId; return ( @@ -340,6 +511,148 @@ export default function UsersPage() { + {/* Cards management */} + + + + + + + + + {cardsUser + ? `${cardsUser.name ?? cardsUser.username ?? cardsUser.email} 的储值卡` + : "储值卡管理"} + + + +
+ +
+ + +
+ setNewTagId(e.target.value)} + /> +
+ +
+ + {tagsLoading ? ( +
+ +
+ ) : userTags.length === 0 ? ( +

+ 该用户暂无储值卡 +

+ ) : ( + + + + + 卡号 + 状态 + 余额 + 操作 + + + {userTags.map((tag) => ( + + + {tag.idTag} + + + + {tag.status} + + + + ¥{fenToYuan(tag.balance)} + + +
+ + +
+
+
+ ))} +
+
+
+
+ )} +
+ + + +
+
+
+
{/* Ban / Unban button — disabled for current account */}