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:
2026-05-25 16:02:23 +08:00
parent ca3a1db770
commit 3bcbf7d256
10 changed files with 446 additions and 30 deletions

View File

@@ -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>
);
})}