"use client"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CalendarRange, Check, ChevronDown, Search } from "lucide-react"; import { useTranslation } from "react-i18next"; import { getDrawCurrent } from "@/api/draw"; import { getTicketItems } from "@/api/ticket-items"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; 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 { LOTTERY_SCHEDULE_TIMEZONE } from "@/lib/lottery-schedule-timezone"; import { formatMinorAsCurrency } from "@/lib/money"; import { isCreditFundingPlayer } from "@/lib/player-funding-mode"; import { formatSchedulePickerYmd, getTimeZoneShortLabel, parseSchedulePickerYmd, scheduleTodayYmd, } from "@/lib/player-datetime"; import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { playLabel } from "@/lib/play-labels"; import { cn } from "@/lib/utils"; import type { TicketItemListRow } from "@/types/api/ticket-items"; const ORDERS_PAGE_SIZE = 20; const STATUS_OPTIONS = [ "pending_confirm", "partial_pending_confirm", "pending_draw", "partial_failed", "pending_payout", "settled_win", "settled_lose", "failed", "refunded", ] as const; export function TicketOrdersListScreen() { const searchParams = useSearchParams(); const { t } = useTranslation("player"); const { activeCurrency } = useActivePlayerCurrency(); const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile)); useCurrencyCatalog(); const drawNoFilter = useMemo(() => (searchParams.get("draw_no") ?? "").trim(), [searchParams]); const statusFilter = useMemo( () => searchParams.getAll("status").map((s) => s.trim()).filter(Boolean), [searchParams], ); const [items, setItems] = useState([]); const [page, setPage] = useState(1); const [lastPage, setLastPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); const [queryDrawNoState, setQueryDrawNoState] = useState(() => ({ base: drawNoFilter, draft: drawNoFilter, })); const [queryNumber, setQueryNumber] = useState(""); const [queryStatuses, setQueryStatuses] = useState(statusFilter); const [fromDate, setFromDate] = useState(""); const [toDate, setToDate] = useState(""); const [rangeOpen, setRangeOpen] = useState(false); const [statusOpen, setStatusOpen] = useState(false); const [calendarMonth, setCalendarMonth] = useState(() => new Date()); const [scheduleTimezone, setScheduleTimezone] = useState(LOTTERY_SCHEDULE_TIMEZONE); const loadMoreRef = useRef(null); const isMobile = useIsMobile(); const initialLoadDone = useRef(false); const queryDrawNoInput = queryDrawNoState.base === drawNoFilter ? queryDrawNoState.draft : drawNoFilter; const queryDrawNo = queryDrawNoInput || drawNoFilter; const selectedRange = useMemo(() => { const from = parseSchedulePickerYmd(fromDate); const to = parseSchedulePickerYmd(toDate); if (!from && !to) return undefined; if (from && to) return { from, to }; if (from) return { from }; return to ? { from: to } : undefined; }, [fromDate, toDate]); const formatCompactDate = useCallback((value: string) => { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); if (!m) return value; return `${m[2]}-${m[3]}`; }, []); const dateLabel = useMemo(() => { if (!fromDate && !toDate) return t("orders.dateRange"); if (fromDate && toDate) return `${formatCompactDate(fromDate)} ~ ${formatCompactDate(toDate)}`; 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) => { if (append) setLoadingMore(true); else setLoading(true); setError(null); try { const res = await getTicketItems({ page: nextPage, per_page: ORDERS_PAGE_SIZE, draw_no: queryDrawNo || undefined, number: queryNumber || undefined, status: queryStatuses.length ? queryStatuses : undefined, start_date: fromDate || undefined, end_date: toDate || undefined, }); setItems((prev) => (append ? [...prev, ...res.items] : res.items)); setPage(res.page); setLastPage(res.last_page); setTotal(res.total); } catch { setError(t("orders.loadFailed")); if (!append) setItems([]); } finally { setLoading(false); setLoadingMore(false); } }, [fromDate, queryDrawNo, queryNumber, queryStatuses, t, toDate], ); useEffect(() => { void getDrawCurrent() .then((res) => { const tz = res.data?.schedule_timezone?.trim(); if (tz) setScheduleTimezone(tz); }) .catch(() => { /* 保留默认排期时区 */ }); }, []); const scheduleTzLabel = useMemo( () => getTimeZoneShortLabel(scheduleTimezone), [scheduleTimezone], ); useEffect(() => { if (!initialLoadDone.current) { initialLoadDone.current = true; void fetchPage(1, false); return; } setItems([]); setPage(1); setLastPage(1); void fetchPage(1, false); }, [fetchPage, queryDrawNo, queryNumber, queryStatuses, fromDate, toDate]); useEffect(() => { const refreshFromEvents = () => { void fetchPage(1, false); }; window.addEventListener("lottery-hall-refresh", refreshFromEvents); window.addEventListener("lottery-wallet-refresh", refreshFromEvents); return () => { window.removeEventListener("lottery-hall-refresh", refreshFromEvents); window.removeEventListener("lottery-wallet-refresh", refreshFromEvents); }; }, [fetchPage]); useEffect(() => { const target = loadMoreRef.current; if (!target || loading || loadingMore || page >= lastPage) return; const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting) { void fetchPage(page + 1, true); } }, { rootMargin: "160px" }, ); observer.observe(target); return () => observer.disconnect(); }, [fetchPage, lastPage, loading, loadingMore, page]); return (

{drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")}

{drawNoFilter || total}

{t("orders.betNow")} {(queryDrawNoInput || queryNumber || fromDate || toDate || queryStatuses.length > 0) ? ( ) : null}
setQueryDrawNoState({ base: drawNoFilter, draft: e.target.value }) } placeholder={t("orders.drawNo")} aria-label={t("orders.drawNo")} className="h-7 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0" />
setQueryNumber(e.target.value)} placeholder={t("orders.number")} aria-label={t("orders.number")} className="h-7 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0" />
{dateLabel} } />

{t("orders.dateRangeHint", { tz: scheduleTzLabel })}

{ if (!range?.from && !range?.to) { setFromDate(""); setToDate(""); return; } setFromDate(range?.from ? formatSchedulePickerYmd(range.from) : ""); setToDate(range?.to ? formatSchedulePickerYmd(range.to) : ""); }} />
{t("orders.status")} {queryStatuses.length ? ( {queryStatuses.length} ) : null} } />

{t("orders.statusFilter")}

{STATUS_OPTIONS.map((status) => { const checked = queryStatuses.includes(status); return ( ); })}
{loading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : error ? (

{error}

) : items.length === 0 ? (

{t("orders.empty")}

{t("orders.submitBet")}
) : ( <>
{orderGroups.map((group) => { const cur = group.currency_code ?? activeCurrency; const st = ticketStatusDisplay( group.status, group.win_amount, group.jackpot_win_amount, t, creditMode, ); const totalWin = group.win_amount + group.jackpot_win_amount; return ( 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]" >

{group.draw_no ?? "—"}

{group.items.map((row) => (

{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}

))}

{t("orders.stake")}

{formatMinorAsCurrency(group.total_bet_amount, cur)}

{t("orders.deduction")}

{formatMinorAsCurrency(group.actual_deduct_amount, cur)}

{group.status === "partial_failed" ? (

{t("orders.partialFailedHint")}

) : null} {totalWin > 0 && group.status === "settled_win" ? (

{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}

) : null} ); })}
{isMobile ?
: null} {isMobile && page < lastPage ? ( ) : !isMobile && lastPage > 1 ? (
{visiblePages.map((p) => ( ))}
) : lastPage > 1 ? (

{t("orders.noMore")}

) : null} )}
); } function buildPageWindow(current: number, last: number): number[] { if (last <= 5) { return Array.from({ length: last }, (_, index) => index + 1); } const start = Math.max(1, Math.min(current - 2, last - 4)); return Array.from({ length: 5 }, (_, index) => start + index); }