From 33f8f82e07cbf8d3975afc0fe0156224130a3256 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 14:49:44 +0800 Subject: [PATCH] fix(shop): manage staff through backend --- .../dashboard/shop/employees/page.tsx | 280 +++++++++++++----- 1 file changed, 198 insertions(+), 82 deletions(-) diff --git a/app/(dashboard)/dashboard/shop/employees/page.tsx b/app/(dashboard)/dashboard/shop/employees/page.tsx index ba83bd0..071c6f0 100644 --- a/app/(dashboard)/dashboard/shop/employees/page.tsx +++ b/app/(dashboard)/dashboard/shop/employees/page.tsx @@ -19,13 +19,20 @@ import { TableHeader, TableRow, } from "@/components/ui/table" -import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" -import { useAuthStore } from "@/store/auth" -import { usePlayerStore } from "@/store/players" -import { useShopStore } from "@/store/shops" +import { + inviteShopPlayer, + listPlayers, + listPlayersByShop, + listShopInvitations, + removeShopPlayer, +} from "@/lib/api" +import { toApiError } from "@/lib/errors" +import { useMyShop } from "@/lib/hooks/use-my-shop" +import { notifyInfo, notifySuccess } from "@/lib/toast" +import type { Player } from "@/lib/types" import { MoreHorizontal, Star, UserPlus } from "lucide-react" import Link from "next/link" -import { useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" const statusLabels: Record = { available: "在线", @@ -41,42 +48,137 @@ const statusVariants: Record = { export default function EmployeesPage() { const [search, setSearch] = useState("") - const userId = useAuthStore((state) => state.user?.id) - const shops = useShopStore((state) => state.shops) - const players = usePlayerStore((state) => state.players) - const assignToShop = usePlayerStore((state) => state.assignToShop) - const removeFromShop = usePlayerStore((state) => state.removeFromShop) - const shop = resolveOwnerShop(userId, shops) - const updateShop = useShopStore((state) => state.updateShop) + const { shop, loading, error, refreshShop } = useMyShop() + const [players, setPlayers] = useState([]) + const [shopPlayers, setShopPlayers] = useState([]) + const [pendingPlayerIds, setPendingPlayerIds] = useState>(new Set()) + const [dataLoading, setDataLoading] = useState(true) + const [saving, setSaving] = useState(false) - const shopPlayers = useMemo(() => { + const loadData = useCallback(async () => { + if (!shop) return + + setDataLoading(true) + try { + const [nextPlayers, nextShopPlayers, invitations] = await Promise.all([ + listPlayers(), + listPlayersByShop(shop.id), + listShopInvitations(shop.id), + ]) + setPlayers(nextPlayers) + setShopPlayers(nextShopPlayers) + setPendingPlayerIds( + new Set( + invitations + .filter((invitation) => invitation.status === "pending") + .map((invitation) => String(invitation.playerId)), + ), + ) + } catch (error) { + notifyInfo(toApiError(error).msg) + } finally { + setDataLoading(false) + } + }, [shop]) + + useEffect(() => { + if (!shop) return + + let cancelled = false + + Promise.all([listPlayers(), listPlayersByShop(shop.id), listShopInvitations(shop.id)]) + .then(([nextPlayers, nextShopPlayers, invitations]) => { + if (cancelled) return + setPlayers(nextPlayers) + setShopPlayers(nextShopPlayers) + setPendingPlayerIds( + new Set( + invitations + .filter((invitation) => invitation.status === "pending") + .map((invitation) => String(invitation.playerId)), + ), + ) + }) + .catch((error) => { + if (cancelled) return + notifyInfo(toApiError(error).msg) + }) + .finally(() => { + if (cancelled) return + setDataLoading(false) + }) + + return () => { + cancelled = true + } + }, [shop]) + + const filteredPlayers = useMemo(() => { if (!shop) return [] - return players.filter( + return shopPlayers.filter( (player) => player.shopId === shop.id && player.user.nickname.toLowerCase().includes(search.trim().toLowerCase()), ) - }, [players, shop, search]) + }, [search, shop, shopPlayers]) const inviteCandidate = useMemo( - () => (shop ? players.find((player) => player.shopId !== shop.id) : undefined), - [players, shop], + () => + shop + ? players.find((player) => player.shopId !== shop.id && !pendingPlayerIds.has(player.id)) + : undefined, + [pendingPlayerIds, players, shop], ) + if (loading) { + return
加载中...
+ } + + if (error) { + return
{error}
+ } + if (!shop) { return
当前账号没有可管理的店铺
} + const handleInvite = async () => { + if (!inviteCandidate) return + + setSaving(true) + try { + await inviteShopPlayer(shop.id, inviteCandidate.id) + await loadData() + notifySuccess("邀请已发送") + } catch (error) { + notifyInfo(toApiError(error).msg) + } finally { + setSaving(false) + } + } + + const handleRemove = async (player: Player) => { + setSaving(true) + try { + await removeShopPlayer(shop.id, player.id) + await Promise.all([loadData(), refreshShop()]) + notifySuccess("打手已移除") + } catch (error) { + notifyInfo(toApiError(error).msg) + } finally { + setSaving(false) + } + } + return (

员工管理

- - - - 查看详情 - - - 调整抽成 - - { - removeFromShop(player.id) - updateShop(shop.id, { playerCount: Math.max(0, shop.playerCount - 1) }) - }} - > - 移除打手 - - - + {dataLoading ? ( + + + 加载中... - ))} + ) : filteredPlayers.length === 0 ? ( + + + 暂无签约打手 + + + ) : ( + filteredPlayers.map((player) => ( + + +
+ + + {player.user.nickname[0]} + +
+

{player.user.nickname}

+

{player.totalOrders} 单

+
+
+
+ +
+ {player.games.map((game) => ( + + {game} + + ))} +
+
+ +
+ + {player.rating} +
+
+ {(player.completionRate * 100).toFixed(0)}% + + + {statusLabels[player.status]} + + + + + + + + + + 查看详情 + + + 调整抽成 + + { + void handleRemove(player) + }} + disabled={saving} + > + 移除打手 + + + + +
+ )) + )}