fix(shop): load orders and income from backend

This commit is contained in:
zetaloop
2026-04-25 14:49:50 +08:00
parent 33f8f82e07
commit c3843b3671
2 changed files with 117 additions and 78 deletions
+43 -53
View File
@@ -10,30 +10,34 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { requestWithAuth } from "@/lib/api" import { getShopIncomeStats } from "@/lib/api"
import type { ShopIncomeStats } from "@/lib/api/shops"
import { listWalletTransactions } from "@/lib/api/transactions" import { listWalletTransactions } from "@/lib/api/transactions"
import { isActiveOrder, isCompletedOrder } from "@/lib/domain/order-filters"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop"
import { toApiError } from "@/lib/errors" import { toApiError } from "@/lib/errors"
import { useMyShop } from "@/lib/hooks/use-my-shop"
import type { WalletTransaction } from "@/lib/types" import type { WalletTransaction } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
import { useOrderStore } from "@/store/orders"
import { useShopStore } from "@/store/shops"
import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react" import { ArrowDownLeft, ArrowUpRight, CreditCard, DollarSign } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
const emptyStats: ShopIncomeStats = {
monthlyIncome: "0",
pendingSettlement: "0",
totalWithdrawn: "0",
totalOrders: 0,
completedOrders: 0,
}
export default function ShopIncomePage() { export default function ShopIncomePage() {
const userId = useAuthStore((state) => state.user?.id) const { shop, loading, error } = useMyShop()
const shops = useShopStore((state) => state.shops)
const orders = useOrderStore((state) => state.orders)
const shop = resolveOwnerShop(userId, shops)
const [transactions, setTransactions] = useState<WalletTransaction[]>([]) const [transactions, setTransactions] = useState<WalletTransaction[]>([])
const [stats, setStats] = useState<ShopIncomeStats>(emptyStats)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadError, setLoadError] = useState<string | null>(null) const [loadError, setLoadError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (!shop) return if (!shop) return
const shopId = shop.id
let cancelled = false let cancelled = false
@@ -41,14 +45,13 @@ export default function ShopIncomePage() {
setIsLoading(true) setIsLoading(true)
setLoadError(null) setLoadError(null)
try { try {
const res = await requestWithAuth(() => listWalletTransactions({ offset: 0, limit: 1000 })) const [nextStats, nextTransactions] = await Promise.all([
getShopIncomeStats(shopId),
listWalletTransactions({ offset: 0, limit: 1000 }),
])
if (cancelled) return if (cancelled) return
if (!res) { setStats(nextStats)
setLoadError("请先登录") setTransactions(nextTransactions)
setTransactions([])
return
}
setTransactions(res)
} catch (error) { } catch (error) {
if (cancelled) return if (cancelled) return
setLoadError(toApiError(error).msg) setLoadError(toApiError(error).msg)
@@ -63,34 +66,21 @@ export default function ShopIncomePage() {
} }
}, [shop]) }, [shop])
if (loading) {
return <div className="text-sm text-muted-foreground">...</div>
}
if (error) {
return <div className="text-sm text-muted-foreground">{error}</div>
}
if (!shop) { if (!shop) {
return <div className="text-sm text-muted-foreground"></div> return <div className="text-sm text-muted-foreground"></div>
} }
const shopOrders = orders.filter((order) => order.shopId === shop?.id) const relatedTransactions = transactions.filter(
const completedOrders = shopOrders.filter((o) => isCompletedOrder(o.status)) (transaction) => transaction.type === "income" || transaction.type === "withdrawal",
const totalIncome = completedOrders.reduce((acc, order) => acc + order.totalPrice, 0) )
const currentMonth = new Date().getMonth()
const thisMonthIncome = completedOrders
.filter((o) => new Date(o.completedAt || "").getMonth() === currentMonth)
.reduce((acc, order) => acc + order.totalPrice, 0)
const pendingSettlement = shopOrders
.filter(
(o) =>
isActiveOrder(o.status) && o.status !== "pending_payment" && o.status !== "pending_accept",
)
.reduce((acc, order) => acc + order.totalPrice, 0)
const shopOrderIds = new Set(shopOrders.map((order) => order.id))
const relatedTransactions = transactions.filter((transaction) => {
if (transaction.type === "withdrawal") return true
if (transaction.type !== "income") return false
const match = transaction.description.match(/ord[-\d]+/)
if (!match) return false
return shopOrderIds.has(match[0])
})
return ( return (
<div className="container mx-auto max-w-6xl px-4 py-8 space-y-8"> <div className="container mx-auto max-w-6xl px-4 py-8 space-y-8">
@@ -99,29 +89,29 @@ export default function ShopIncomePage() {
<div className="grid gap-4 sm:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-3">
<Card className="hover:shadow-card-hover"> <Card className="hover:shadow-card-hover">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle> <CardTitle className="text-sm font-medium"></CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">¥{totalIncome.toFixed(2)}</div> <div className="text-2xl font-bold">¥{stats.monthlyIncome}</div>
</CardContent>
</Card>
<Card className="hover:shadow-card-hover">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{thisMonthIncome.toFixed(2)}</div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="hover:shadow-card-hover"> <Card className="hover:shadow-card-hover">
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle> <CardTitle className="text-sm font-medium"></CardTitle>
<CreditCard className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">¥{stats.pendingSettlement}</div>
</CardContent>
</Card>
<Card className="hover:shadow-card-hover">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<ArrowUpRight className="h-4 w-4 text-muted-foreground" /> <ArrowUpRight className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">¥{pendingSettlement.toFixed(2)}</div> <div className="text-2xl font-bold">¥{stats.totalWithdrawn}</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+74 -25
View File
@@ -11,27 +11,62 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { listOrders } from "@/lib/api"
import { statusLabels } from "@/lib/constants" import { statusLabels } from "@/lib/constants"
import { isActiveOrder, isCompletedOrder, isDisputedOrder } from "@/lib/domain/order-filters" import { isActiveOrder, isCompletedOrder, isDisputedOrder } from "@/lib/domain/order-filters"
import { resolveOwnerShop } from "@/lib/domain/resolve-current-shop" import { toApiError } from "@/lib/errors"
import { useAuthStore } from "@/store/auth" import { useMyShop } from "@/lib/hooks/use-my-shop"
import { useOrderStore } from "@/store/orders" import { notifyInfo } from "@/lib/toast"
import { useShopStore } from "@/store/shops"
import { AlertCircle, CheckCircle, Clock, ListOrdered } from "lucide-react" import { AlertCircle, CheckCircle, Clock, ListOrdered } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useEffect, useMemo, useState } from "react"
export default function ShopOrdersPage() { export default function ShopOrdersPage() {
const userId = useAuthStore((state) => state.user?.id) const { shop, loading, error } = useMyShop()
const shops = useShopStore((state) => state.shops) const [orders, setOrders] = useState<Awaited<ReturnType<typeof listOrders>>>([])
const orders = useOrderStore((state) => state.orders) const [ordersLoading, setOrdersLoading] = useState(true)
const shop = resolveOwnerShop(userId, shops)
useEffect(() => {
if (!shop) return
let cancelled = false
listOrders({ role: "owner" })
.then((items) => {
if (cancelled) return
setOrders(items)
})
.catch((error) => {
if (cancelled) return
notifyInfo(toApiError(error).msg)
})
.finally(() => {
if (cancelled) return
setOrdersLoading(false)
})
return () => {
cancelled = true
}
}, [shop])
const shopOrders = useMemo(
() => (shop ? orders.filter((order) => order.shopId === shop.id) : []),
[orders, shop],
)
if (loading) {
return <div className="text-sm text-muted-foreground">...</div>
}
if (error) {
return <div className="text-sm text-muted-foreground">{error}</div>
}
if (!shop) { if (!shop) {
return <div className="text-sm text-muted-foreground"></div> return <div className="text-sm text-muted-foreground"></div>
} }
const shopOrders = orders.filter((order) => order.shopId === shop?.id)
const totalOrders = shopOrders.length const totalOrders = shopOrders.length
const activeOrders = shopOrders.filter((o) => isActiveOrder(o.status)).length const activeOrders = shopOrders.filter((o) => isActiveOrder(o.status)).length
const completedOrders = shopOrders.filter((o) => isCompletedOrder(o.status)).length const completedOrders = shopOrders.filter((o) => isCompletedOrder(o.status)).length
@@ -100,23 +135,37 @@ export default function ShopOrdersPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{shopOrders.map((order) => ( {ordersLoading ? (
<TableRow key={order.id}> <TableRow>
<TableCell className="font-medium">{order.service.title}</TableCell> <TableCell colSpan={7} className="text-center text-sm text-muted-foreground">
<TableCell>{order.consumerId}</TableCell> ...
<TableCell>{order.playerId}</TableCell>
<TableCell>
<Badge variant="outline">{statusLabels[order.status]}</Badge>
</TableCell>
<TableCell>¥{order.totalPrice}</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link href={`/order/${order.id}`}></Link>
</Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ) : shopOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-sm text-muted-foreground">
</TableCell>
</TableRow>
) : (
shopOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{order.service.title}</TableCell>
<TableCell>{order.consumerId}</TableCell>
<TableCell>{order.playerId}</TableCell>
<TableCell>
<Badge variant="outline">{statusLabels[order.status]}</Badge>
</TableCell>
<TableCell>¥{order.totalPrice}</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link href={`/order/${order.id}`}></Link>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>