feat: enhance ticket order detail and status display
- Updated ticket status display logic to handle "failed" status and improve "settled_win" condition. - Refactored TicketOrderDetailScreen to utilize search parameters for dynamic navigation and grouping of tickets. - Enhanced TicketOrdersListScreen to group ticket items and improve rendering of order details. - Added new translations for order-related terms in multiple languages to support enhanced user experience.
This commit is contained in:
10
src/app/(player)/(main)/orders/group/[groupKey]/page.tsx
Normal file
10
src/app/(player)/(main)/orders/group/[groupKey]/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TicketOrderGroupScreen } from "@/features/orders/ticket-order-group-screen";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ groupKey: string }>;
|
||||
};
|
||||
|
||||
export default async function OrderGroupPage({ params }: PageProps) {
|
||||
const { groupKey } = await params;
|
||||
return <TicketOrderGroupScreen groupKey={groupKey} />;
|
||||
}
|
||||
131
src/features/orders/group-ticket-items.ts
Normal file
131
src/features/orders/group-ticket-items.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { TicketItemListRow } from "@/types/api/ticket-items";
|
||||
|
||||
export type TicketItemGroup = {
|
||||
key: string;
|
||||
order_no: string | null;
|
||||
draw_no: string | null;
|
||||
placed_at: string | null;
|
||||
currency_code: string | null;
|
||||
status: string;
|
||||
items: TicketItemListRow[];
|
||||
total_bet_amount: number;
|
||||
actual_deduct_amount: number;
|
||||
win_amount: number;
|
||||
jackpot_win_amount: number;
|
||||
};
|
||||
|
||||
export const ORDER_GROUP_STORAGE_PREFIX = "lottery:order-group:v1:";
|
||||
|
||||
/** 分组键:有 order_no 则按订单;否则 draw_no + placed_at + currency + status 兜底 */
|
||||
export function getTicketItemGroupKey(row: TicketItemListRow): string {
|
||||
const orderNo = (row.order_no ?? "").trim();
|
||||
if (orderNo) {
|
||||
return `order:${orderNo}`;
|
||||
}
|
||||
const drawNo = row.draw_no ?? "";
|
||||
const placedAt = row.placed_at ?? "";
|
||||
const currency = row.currency_code ?? "";
|
||||
const status = row.status ?? "";
|
||||
return `fallback:${drawNo}|${placedAt}|${currency}|${status}`;
|
||||
}
|
||||
|
||||
export function sortTicketGroupItems(items: TicketItemListRow[]): TicketItemListRow[] {
|
||||
return [...items].sort((a, b) => {
|
||||
const byPlay = a.play_code.localeCompare(b.play_code);
|
||||
if (byPlay !== 0) return byPlay;
|
||||
return (a.original_number ?? "").localeCompare(b.original_number ?? "");
|
||||
});
|
||||
}
|
||||
|
||||
/** 保持 API 返回顺序下,按分组键首次出现顺序输出合并组 */
|
||||
export function groupTicketItems(items: TicketItemListRow[]): TicketItemGroup[] {
|
||||
const map = new Map<string, TicketItemGroup>();
|
||||
const order: string[] = [];
|
||||
|
||||
for (const row of items) {
|
||||
const key = getTicketItemGroupKey(row);
|
||||
let group = map.get(key);
|
||||
if (!group) {
|
||||
const orderNo = (row.order_no ?? "").trim();
|
||||
group = {
|
||||
key,
|
||||
order_no: orderNo || null,
|
||||
draw_no: row.draw_no ?? null,
|
||||
placed_at: row.placed_at ?? null,
|
||||
currency_code: row.currency_code ?? null,
|
||||
status: row.status,
|
||||
items: [],
|
||||
total_bet_amount: 0,
|
||||
actual_deduct_amount: 0,
|
||||
win_amount: 0,
|
||||
jackpot_win_amount: 0,
|
||||
};
|
||||
map.set(key, group);
|
||||
order.push(key);
|
||||
}
|
||||
group.items.push(row);
|
||||
group.total_bet_amount += row.total_bet_amount;
|
||||
group.actual_deduct_amount += row.actual_deduct_amount;
|
||||
group.win_amount += row.win_amount;
|
||||
group.jackpot_win_amount += row.jackpot_win_amount;
|
||||
}
|
||||
|
||||
return order.map((key) => {
|
||||
const group = map.get(key)!;
|
||||
return { ...group, items: sortTicketGroupItems(group.items) };
|
||||
});
|
||||
}
|
||||
|
||||
export function persistOrderGroup(group: TicketItemGroup): void {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
ORDER_GROUP_STORAGE_PREFIX + group.key,
|
||||
JSON.stringify(group),
|
||||
);
|
||||
} catch {
|
||||
/* quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
export function loadPersistedOrderGroup(groupKey: string): TicketItemGroup | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(ORDER_GROUP_STORAGE_PREFIX + groupKey);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as TicketItemGroup;
|
||||
if (!parsed?.key || !Array.isArray(parsed.items)) return null;
|
||||
return { ...parsed, items: sortTicketGroupItems(parsed.items) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function orderGroupPath(groupKey: string, ticketNos?: string[]): string {
|
||||
const base = `/orders/group/${encodeURIComponent(groupKey)}`;
|
||||
if (!ticketNos?.length) return base;
|
||||
return `${base}?tickets=${encodeURIComponent(ticketNos.join(","))}`;
|
||||
}
|
||||
|
||||
export function orderGroupHref(group: TicketItemGroup): string {
|
||||
if (group.items.length === 1) {
|
||||
return `/orders/${encodeURIComponent(group.items[0].ticket_no)}`;
|
||||
}
|
||||
return orderGroupPath(
|
||||
group.key,
|
||||
group.items.map((i) => i.ticket_no),
|
||||
);
|
||||
}
|
||||
|
||||
/** 注项详情;来自订单组时带上 fromGroup,便于返回订单详情 */
|
||||
export function ticketDetailHref(
|
||||
ticketNo: string,
|
||||
fromGroup?: TicketItemGroup | null,
|
||||
): string {
|
||||
const base = `/orders/${encodeURIComponent(ticketNo)}`;
|
||||
const key = (fromGroup?.key ?? "").trim();
|
||||
if (!key) return base;
|
||||
const params = new URLSearchParams({ fromGroup: key });
|
||||
if (fromGroup && fromGroup.items.length > 1) {
|
||||
params.set("tickets", fromGroup.items.map((i) => i.ticket_no).join(","));
|
||||
}
|
||||
return `${base}?${params.toString()}`;
|
||||
}
|
||||
27
src/features/orders/order-meta-line.tsx
Normal file
27
src/features/orders/order-meta-line.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
|
||||
type OrderMetaLineProps = {
|
||||
orderNo: string | null | undefined;
|
||||
placedAt: string | null | undefined;
|
||||
t: TFunction<"player">;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** 订单号 + 下单时间(与 hall.result.orderNo 文案一致) */
|
||||
export function OrderMetaLine({ orderNo, placedAt, t, className }: OrderMetaLineProps) {
|
||||
const trimmed = (orderNo ?? "").trim();
|
||||
const displayNo = trimmed || t("orders.noOrderNo");
|
||||
|
||||
return (
|
||||
<p className={className ?? "mt-1 truncate text-xs text-slate-500"}>
|
||||
<span>{t("orders.orderNoLabel")}</span>{" "}
|
||||
<span className="font-mono font-semibold text-slate-600">{displayNo}</span>
|
||||
<span aria-hidden> · </span>
|
||||
<span>{formatLotteryInstant(placedAt ?? null)}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -19,13 +19,16 @@ export function ticketStatusDisplay(
|
||||
if (status === "settled_win" && total > 0) {
|
||||
return { label: t?.("ticketStatus.settled_win") ?? status, dotClass: "bg-emerald-500" };
|
||||
}
|
||||
if (status === "settled_lose" || status === "settled_win") {
|
||||
if (status === "settled_lose" || (status === "settled_win" && total <= 0)) {
|
||||
return {
|
||||
label: t?.("ticketStatus.settled_lose") ?? status,
|
||||
dotClass: "bg-background",
|
||||
ring: true,
|
||||
};
|
||||
}
|
||||
if (status === "failed") {
|
||||
return { label: t?.("ticketStatus.failed") ?? status, dotClass: "bg-red-500" };
|
||||
}
|
||||
return {
|
||||
label: t?.("ticketStatus.unknown", { status, defaultValue: status }) ?? status,
|
||||
dotClass: "bg-red-500",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { getTicketItemDetail } from "@/api/ticket-items";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { TwentyThreeResultsGrid } from "@/features/results/twenty-three-results-grid";
|
||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||
import { orderGroupPath } from "@/features/orders/group-ticket-items";
|
||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
@@ -67,6 +69,7 @@ type TicketItemDetailWithExtras = TicketItemDetailPayload & {
|
||||
|
||||
/** 界面文档 §4.8 注单详情 */
|
||||
export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation("player");
|
||||
const { activeCurrency } = useActivePlayerCurrency();
|
||||
useCurrencyCatalog();
|
||||
@@ -74,6 +77,27 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fromGroupKey = useMemo(
|
||||
() => (searchParams.get("fromGroup") ?? "").trim(),
|
||||
[searchParams],
|
||||
);
|
||||
const groupTickets = useMemo(
|
||||
() => (searchParams.get("tickets") ?? "").trim(),
|
||||
[searchParams],
|
||||
);
|
||||
const backNav = useMemo(() => {
|
||||
if (!fromGroupKey) {
|
||||
return { href: "/orders", label: t("orders.title") };
|
||||
}
|
||||
const ticketNos = groupTickets
|
||||
? groupTickets.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
return {
|
||||
href: orderGroupPath(fromGroupKey, ticketNos),
|
||||
label: t("orders.groupDetail"),
|
||||
};
|
||||
}, [fromGroupKey, groupTickets, t]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -98,8 +122,8 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("orders.betDetail")}
|
||||
backHref="/orders"
|
||||
backLabel={t("orders.title")}
|
||||
backHref={backNav.href}
|
||||
backLabel={backNav.label}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-12 rounded-xl" />
|
||||
@@ -113,8 +137,8 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("orders.betDetail")}
|
||||
backHref="/orders"
|
||||
backLabel={t("orders.title")}
|
||||
backHref={backNav.href}
|
||||
backLabel={backNav.label}
|
||||
>
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700">
|
||||
<p>{error ?? t("orders.noData")}</p>
|
||||
@@ -174,8 +198,8 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("orders.betDetail")}
|
||||
backHref="/orders"
|
||||
backLabel={t("orders.title")}
|
||||
backHref={backNav.href}
|
||||
backLabel={backNav.label}
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Card className="ring-0 border border-[#e8eef7] bg-white shadow-[0_8px_28px_rgba(15,23,42,0.05)]">
|
||||
@@ -368,12 +392,12 @@ export function TicketOrderDetailScreen({ ticketNo }: { ticketNo: string }) {
|
||||
</Link>
|
||||
) : null}
|
||||
<Link
|
||||
href="/orders"
|
||||
href={backNav.href}
|
||||
className={cn(
|
||||
"inline-flex h-11 min-w-[140px] flex-1 items-center justify-center rounded-xl border border-[#dce7f7] bg-white px-4 text-sm font-semibold text-[#07459f] transition-colors hover:bg-[#f1f6ff]",
|
||||
)}
|
||||
>
|
||||
{t("orders.backToOrders")}
|
||||
{fromGroupKey ? t("orders.backToGroup") : t("orders.backToOrders")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
167
src/features/orders/ticket-order-group-screen.tsx
Normal file
167
src/features/orders/ticket-order-group-screen.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||
import { OrderMetaLine } from "@/features/orders/order-meta-line";
|
||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||
import {
|
||||
loadPersistedOrderGroup,
|
||||
ticketDetailHref,
|
||||
type TicketItemGroup,
|
||||
} from "@/features/orders/group-ticket-items";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { playLabel } from "@/lib/play-labels";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TicketOrderGroupScreenProps = {
|
||||
groupKey: string;
|
||||
};
|
||||
|
||||
export function TicketOrderGroupScreen({ groupKey }: TicketOrderGroupScreenProps) {
|
||||
const { t } = useTranslation("player");
|
||||
const { activeCurrency } = useActivePlayerCurrency();
|
||||
useCurrencyCatalog();
|
||||
|
||||
const decodedKey = decodeURIComponent(groupKey);
|
||||
|
||||
const group = useMemo(
|
||||
(): TicketItemGroup | null => loadPersistedOrderGroup(decodedKey),
|
||||
[decodedKey],
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("orders.groupDetail")}
|
||||
backHref="/orders"
|
||||
backLabel={t("orders.title")}
|
||||
>
|
||||
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-[#f8fbff] px-3 py-8 text-center">
|
||||
<p className="text-sm font-bold text-slate-700">{t("orders.groupNotFound")}</p>
|
||||
<Link
|
||||
href="/orders"
|
||||
className="mt-4 inline-flex h-9 items-center rounded-lg bg-[#07459f] px-4 text-sm font-bold text-white"
|
||||
>
|
||||
{t("orders.backToOrders")}
|
||||
</Link>
|
||||
</div>
|
||||
</PlayerPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const cur = group.currency_code ?? activeCurrency;
|
||||
const st = ticketStatusDisplay(
|
||||
group.status,
|
||||
group.win_amount,
|
||||
group.jackpot_win_amount,
|
||||
t,
|
||||
);
|
||||
const totalWin = group.win_amount + group.jackpot_win_amount;
|
||||
return (
|
||||
<PlayerPanel
|
||||
title={t("orders.groupDetail")}
|
||||
backHref="/orders"
|
||||
backLabel={t("orders.title")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-xl border border-[#e5edf8] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="truncate font-mono text-lg font-black text-[#0b3f96]">
|
||||
{group.draw_no ?? "—"}
|
||||
</p>
|
||||
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
|
||||
</div>
|
||||
<OrderMetaLine orderNo={group.order_no} placedAt={group.placed_at} t={t} />
|
||||
<p className="mt-2 text-[11px] font-bold uppercase tracking-wide text-[#7890b8]">
|
||||
{t("orders.itemCount", { count: group.items.length })}
|
||||
</p>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
|
||||
<p className="mt-1 text-sm font-black text-slate-900">
|
||||
{formatMinorAsCurrency(group.total_bet_amount, cur)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
|
||||
<p className="mt-1 text-sm font-black text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(group.actual_deduct_amount, cur)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{totalWin > 0 && group.status === "settled_win" ? (
|
||||
<p className="mt-2 text-xs font-bold text-emerald-600">
|
||||
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="px-0.5 text-sm font-black text-[#0b3f96]">{t("orders.betItems")}</p>
|
||||
<div className="space-y-2">
|
||||
{group.items.map((row, index) => {
|
||||
const lineCur = row.currency_code ?? cur;
|
||||
const lineSt = ticketStatusDisplay(
|
||||
row.status,
|
||||
row.win_amount,
|
||||
row.jackpot_win_amount,
|
||||
t,
|
||||
);
|
||||
const lineWin = row.win_amount + row.jackpot_win_amount;
|
||||
return (
|
||||
<Link
|
||||
key={row.ticket_no}
|
||||
href={ticketDetailHref(row.ticket_no, group)}
|
||||
aria-label={t("orders.viewBetLine")}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl border border-[#e5edf8] bg-white px-3 py-3",
|
||||
"shadow-[0_6px_18px_rgba(15,23,42,0.04)] transition-colors hover:border-[#b9ccf6]",
|
||||
)}
|
||||
>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-[#eaf2ff] text-xs font-black text-[#0b56b7]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-black text-[#0b3f96]">
|
||||
{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate font-mono text-[11px] text-slate-500">
|
||||
{t("orders.ticketNo", { ticketNo: row.ticket_no })}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-slate-600">
|
||||
<span>
|
||||
{t("orders.stake")}{" "}
|
||||
<span className="font-bold tabular-nums text-slate-900">
|
||||
{formatMinorAsCurrency(row.total_bet_amount, lineCur)}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
{t("orders.deduction")}{" "}
|
||||
<span className="font-bold tabular-nums text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(row.actual_deduct_amount, lineCur)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{lineWin > 0 && row.status === "settled_win" ? (
|
||||
<p className="mt-1 text-xs font-bold text-emerald-600">
|
||||
{t("orders.win", { amount: formatMinorAsCurrency(lineWin, lineCur) })}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<StatusDot label={lineSt.label} dotClass={lineSt.dotClass} ring={lineSt.ring} />
|
||||
<ChevronRight className="size-4 text-[#7890b8]" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PlayerPanel>
|
||||
);
|
||||
}
|
||||
@@ -14,11 +14,16 @@ import { Input } from "@/components/ui/input";
|
||||
import { PlayerPanel } from "@/components/layout/player-panel";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
groupTicketItems,
|
||||
orderGroupHref,
|
||||
persistOrderGroup,
|
||||
} from "@/features/orders/group-ticket-items";
|
||||
import { OrderMetaLine } from "@/features/orders/order-meta-line";
|
||||
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
|
||||
import { useCurrencyCatalog } from "@/hooks/use-currency-catalog";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { formatLotteryInstant } from "@/lib/player-datetime";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { playLabel } from "@/lib/play-labels";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -92,6 +97,7 @@ export function TicketOrdersListScreen() {
|
||||
return `${fromDate ? formatCompactDate(fromDate) : "..." } ~ ${toDate ? formatCompactDate(toDate) : "..."}`;
|
||||
}, [formatCompactDate, fromDate, t, toDate]);
|
||||
const visiblePages = useMemo(() => buildPageWindow(page, lastPage), [lastPage, page]);
|
||||
const orderGroups = useMemo(() => groupTicketItems(items), [items]);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (nextPage: number, append: boolean) => {
|
||||
@@ -354,49 +360,62 @@ export function TicketOrdersListScreen() {
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{items.map((row) => {
|
||||
const cur = row.currency_code ?? activeCurrency;
|
||||
const st = ticketStatusDisplay(row.status, row.win_amount, row.jackpot_win_amount, t);
|
||||
const totalWin = row.win_amount + row.jackpot_win_amount;
|
||||
{orderGroups.map((group) => {
|
||||
const cur = group.currency_code ?? activeCurrency;
|
||||
const st = ticketStatusDisplay(
|
||||
group.status,
|
||||
group.win_amount,
|
||||
group.jackpot_win_amount,
|
||||
t,
|
||||
);
|
||||
const totalWin = group.win_amount + group.jackpot_win_amount;
|
||||
return (
|
||||
<Link
|
||||
key={row.ticket_no}
|
||||
href={`/orders/${encodeURIComponent(row.ticket_no)}`}
|
||||
key={group.key}
|
||||
href={orderGroupHref(group)}
|
||||
onClick={() => persistOrderGroup(group)}
|
||||
className="block rounded-xl border border-[#e5edf8] bg-white p-3 shadow-[0_8px_24px_rgba(15,23,42,0.05)] transition-colors hover:border-[#b9ccf6]"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-mono text-sm font-black text-[#0b3f96]">
|
||||
{row.draw_no ?? "—"}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-slate-500">
|
||||
<p className="min-w-0 truncate font-mono text-sm font-black text-[#0b3f96]">
|
||||
{group.draw_no ?? "—"}
|
||||
</p>
|
||||
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
|
||||
</div>
|
||||
<OrderMetaLine
|
||||
orderNo={group.order_no}
|
||||
placedAt={group.placed_at}
|
||||
t={t}
|
||||
/>
|
||||
<div className="mt-2.5 space-y-1">
|
||||
{group.items.map((row) => (
|
||||
<p
|
||||
key={row.ticket_no}
|
||||
className="text-sm font-semibold text-[#32518d]"
|
||||
>
|
||||
{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}
|
||||
</p>
|
||||
</div>
|
||||
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.stake")}</p>
|
||||
<p className="mt-1 text-sm font-black text-slate-900">
|
||||
{formatMinorAsCurrency(row.total_bet_amount, cur)}
|
||||
{formatMinorAsCurrency(group.total_bet_amount, cur)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
|
||||
<p className="text-[10px] font-bold uppercase text-[#7890b8]">{t("orders.deduction")}</p>
|
||||
<p className="mt-1 text-sm font-black text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
|
||||
{formatMinorAsCurrency(group.actual_deduct_amount, cur)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{totalWin > 0 && row.status === "settled_win" ? (
|
||||
{totalWin > 0 && group.status === "settled_win" ? (
|
||||
<p className="mt-2 text-xs font-bold text-emerald-600">
|
||||
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-2 text-[11px] text-slate-500">
|
||||
{formatLotteryInstant(row.placed_at ?? null)}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -396,6 +396,14 @@
|
||||
"detailTitle": "Ticket detail",
|
||||
"ticketNo": "Ticket {{ticketNo}}",
|
||||
"orderNo": "Order {{orderNo}}",
|
||||
"orderNoLabel": "Order No.",
|
||||
"orderNoAt": "Order No. {{orderNo}} · {{time}}",
|
||||
"noOrderNo": "—",
|
||||
"groupDetail": "Order detail",
|
||||
"groupNotFound": "Could not load this order. Please open it again from My Bets.",
|
||||
"betItems": "Bet lines",
|
||||
"itemCount": "{{count}} bet line(s)",
|
||||
"viewBetLine": "View bet line detail",
|
||||
"drawNo": "Issue",
|
||||
"placedAt": "Placed at",
|
||||
"number": "Number",
|
||||
@@ -421,6 +429,7 @@
|
||||
"settledAt": "Settled at {{time}}",
|
||||
"viewDraw": "View this draw",
|
||||
"backToOrders": "Back to My Bets",
|
||||
"backToGroup": "Back to order detail",
|
||||
"notFound": "Ticket does not exist or cannot be viewed.",
|
||||
"noData": "No data",
|
||||
"loadFailed": "Failed to load"
|
||||
@@ -555,6 +564,7 @@
|
||||
"pending_payout": "Won, pending payout",
|
||||
"settled_win": "Paid",
|
||||
"settled_lose": "Not won",
|
||||
"failed": "Failed",
|
||||
"unknown": "{{status}}"
|
||||
},
|
||||
"prizeTier": {
|
||||
|
||||
@@ -396,6 +396,14 @@
|
||||
"detailTitle": "टिकट विवरण",
|
||||
"ticketNo": "टिकट {{ticketNo}}",
|
||||
"orderNo": "अर्डर {{orderNo}}",
|
||||
"orderNoLabel": "अर्डर नं.",
|
||||
"orderNoAt": "अर्डर नं. {{orderNo}} · {{time}}",
|
||||
"noOrderNo": "—",
|
||||
"groupDetail": "अर्डर विवरण",
|
||||
"groupNotFound": "यो अर्डर लोड हुन सकेन। कृपया मेरा बेटबाट फेरि खोल्नुहोस्।",
|
||||
"betItems": "बेट लाइन विवरण",
|
||||
"itemCount": "जम्मा {{count}} बेट लाइन",
|
||||
"viewBetLine": "बेट लाइन विवरण हेर्नुहोस्",
|
||||
"drawNo": "इश्यू",
|
||||
"placedAt": "राखेको समय",
|
||||
"number": "नम्बर",
|
||||
@@ -413,9 +421,15 @@
|
||||
"jackpotAmount": "Jackpot {{amount}}",
|
||||
"payoutTotal": "कुल भुक्तानी {{amount}}",
|
||||
"matchLose": "मिलान नतिजा: जितेन",
|
||||
"matchResult": "मिलान नतिजा",
|
||||
"drawPendingMatch": "यस इश्यूका ड्र नम्बर प्रकाशित भएका छैनन्। जित-नजित अझै निर्धारण गर्न मिल्दैन।",
|
||||
"matchPendingDraw": "ड्र पर्खँदैछ। जित-नजित अझै निर्धारण गर्न मिल्दैन।",
|
||||
"matchPendingSettlement": "ड्र नतिजा प्रकाशित भयो। सेटल पछि जित-नजित देखाइनेछ।",
|
||||
"timeline": "समयरेखा",
|
||||
"settledAt": "सेटल समय {{time}}",
|
||||
"viewDraw": "यो ड्र हेर्नुहोस्",
|
||||
"backToOrders": "मेरा बेटमा फर्कनुहोस्",
|
||||
"backToGroup": "अर्डर विवरणमा फर्कनुहोस्",
|
||||
"notFound": "टिकट छैन वा हेर्न अनुमति छैन।",
|
||||
"noData": "डेटा छैन",
|
||||
"loadFailed": "लोड असफल"
|
||||
@@ -550,6 +564,7 @@
|
||||
"pending_payout": "जितेको, भुक्तानी बाँकी",
|
||||
"settled_win": "भुक्तानी भयो",
|
||||
"settled_lose": "जितेन",
|
||||
"failed": "असफल",
|
||||
"unknown": "{{status}}"
|
||||
},
|
||||
"prizeTier": {
|
||||
|
||||
@@ -396,6 +396,14 @@
|
||||
"detailTitle": "注单详情",
|
||||
"ticketNo": "注单号 {{ticketNo}}",
|
||||
"orderNo": "订单 {{orderNo}}",
|
||||
"orderNoLabel": "订单号",
|
||||
"orderNoAt": "订单号 {{orderNo}} · {{time}}",
|
||||
"noOrderNo": "—",
|
||||
"groupDetail": "订单详情",
|
||||
"groupNotFound": "无法加载该订单,请从注单列表重新进入。",
|
||||
"betItems": "注项明细",
|
||||
"itemCount": "共 {{count}} 条注项",
|
||||
"viewBetLine": "查看注项详情",
|
||||
"drawNo": "期号",
|
||||
"placedAt": "下单时间",
|
||||
"number": "号码",
|
||||
@@ -421,6 +429,7 @@
|
||||
"settledAt": "结算时间 {{time}}",
|
||||
"viewDraw": "查看本期开奖",
|
||||
"backToOrders": "返回我的注单",
|
||||
"backToGroup": "返回订单详情",
|
||||
"notFound": "注单不存在或无权查看",
|
||||
"noData": "无数据",
|
||||
"loadFailed": "加载失败"
|
||||
@@ -555,6 +564,7 @@
|
||||
"pending_payout": "已中奖待派彩",
|
||||
"settled_win": "已派彩",
|
||||
"settled_lose": "未中奖",
|
||||
"failed": "失败",
|
||||
"unknown": "{{status}}"
|
||||
},
|
||||
"prizeTier": {
|
||||
|
||||
Reference in New Issue
Block a user