feat: add D4 play grouping with tab navigation and improve header layout
Some checks failed
lotteryfront CI / build (push) Has been cancelled

- Introduced D4 play groups (big/small, position, combo, roll/straight, head/tail/odd/even, digit big/small) with tab-based filtering to improve betting grid usability
- Updated PlayerPanel header grid layout from percentage-based to auto/1fr/auto columns with increased gap for better responsiveness
- Removed "placed" status from ticket orders list filter options
- Added D4 group translations in English, Nepali, and Chinese
- Refactored play columns
This commit is contained in:
2026-06-24 11:18:45 +08:00
parent 588d62a381
commit f46ccaac6e
6 changed files with 90 additions and 10 deletions

View File

@@ -135,6 +135,23 @@ const D4_PLAY_ORDER = [
"digit_small",
] as const;
type D4PlayGroupId =
| "big_small"
| "position"
| "combo"
| "roll_straight"
| "head_tail_odd_even"
| "digit_big_small";
const D4_PLAY_GROUPS: { id: D4PlayGroupId; labelKey: string; playCodes: readonly string[] }[] = [
{ id: "big_small", labelKey: "hall.d4Group.big_small", playCodes: ["big", "small"] },
{ id: "position", labelKey: "hall.d4Group.position", playCodes: ["pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e"] },
{ id: "combo", labelKey: "hall.d4Group.combo", playCodes: ["box", "ibox", "mbox"] },
{ id: "roll_straight", labelKey: "hall.d4Group.roll_straight", playCodes: ["roll", "straight"] },
{ id: "head_tail_odd_even", labelKey: "hall.d4Group.head_tail_odd_even", playCodes: ["head", "tail", "odd", "even"] },
{ id: "digit_big_small", labelKey: "hall.d4Group.digit_big_small", playCodes: ["digit_big", "digit_small"] },
];
const categoryPlayOrders: Record<Exclude<HallCategory, "JACKPOT">, readonly string[]> = {
D2: D2_PLAY_ORDER,
D3: D3_PLAY_ORDER,
@@ -446,6 +463,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
const [d4PlayGroup, setD4PlayGroup] = useState<D4PlayGroupId>("big_small");
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
const [activeRowId, setActiveRowId] = useState<string | null>(null);
const [catalogState, setCatalogState] = useState<
@@ -557,11 +575,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
);
}, [activeCategory, catalogState, openPlays]);
const playColumns = useMemo(() => {
const allPlayColumns = useMemo(() => {
if (activeCategory === "JACKPOT") return [];
return playColumnsForCategory(categoryPlays, activeCategory);
}, [activeCategory, categoryPlays]);
const playColumns = useMemo(() => {
if (activeCategory !== "D4") return allPlayColumns;
const group = D4_PLAY_GROUPS.find((g) => g.id === d4PlayGroup);
if (!group) return allPlayColumns;
const allowedCodes = new Set(group.playCodes);
return allPlayColumns.filter((col) => allowedCodes.has(col.play.play_code));
}, [activeCategory, allPlayColumns, d4PlayGroup]);
const tableMinWidthPx = useMemo(() => {
const indexCol = 40;
const numberCol = activeCategory === "D4" ? 112 : activeCategory === "D3" ? 88 : 72;
@@ -570,7 +596,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
return indexCol + numberCol + playColumns.length * amountCol + deleteCol;
}, [activeCategory, playColumns.length]);
const showWideTableHint = activeCategory === "D4" && playColumns.length > 8;
const showWideTableHint = activeCategory === "D4" && allPlayColumns.length > 8 && playColumns.length > 8;
const activeRow = useMemo(
() => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null,
@@ -598,7 +624,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory !== "D4") return defaultNumberPlaceholder;
const targetRow = activeRow ?? rows[0];
if (!targetRow) return defaultNumberPlaceholder;
const hasRollStake = playColumns.some((column) => {
const hasRollStake = allPlayColumns.some((column) => {
if (column.play.play_code !== "roll") return false;
const amount = parseDecimalInputToMinor(targetRow.amounts[column.key] ?? "", currencyCode);
return amount !== null && amount > 0;
@@ -609,11 +635,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
activeRow,
currencyCode,
defaultNumberPlaceholder,
playColumns,
allPlayColumns,
rows,
t,
]);
const availableD4Groups = useMemo(() => {
if (activeCategory !== "D4") return [];
return D4_PLAY_GROUPS.filter((group) => {
const allowedCodes = new Set(group.playCodes);
return allPlayColumns.some((col) => allowedCodes.has(col.play.play_code));
});
}, [activeCategory, allPlayColumns]);
const updateRowNumber = (id: string, value: string) => {
setRows((current) =>
current.map((row) =>
@@ -809,7 +843,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return [];
const issues: DraftLineIssue[] = [];
rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => {
allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return;
const reason = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot);
@@ -823,7 +857,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
});
return issues;
}, [activeCategory, currencyCode, playColumns, rows]);
}, [activeCategory, currencyCode, allPlayColumns, rows]);
const formatDraftLineIssue = useCallback(
(issue: DraftLineIssue): string => {
@@ -837,7 +871,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return [];
const entries: DraftEntry[] = [];
rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => {
allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return;
const line = lineForPlay(activeCategory, column.play, row.number, amount, column.digitSlot);
@@ -855,7 +889,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
});
});
return entries;
}, [activeCategory, currencyCode, playColumns, rows]);
}, [activeCategory, currencyCode, allPlayColumns, rows]);
const draftEntries = collectEntries();
const draftSummary = useMemo(() => {
@@ -1155,6 +1189,29 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div>
</div>
{activeCategory === "D4" && availableD4Groups.length > 1 ? (
<div className="flex gap-1 overflow-x-auto overscroll-x-contain pb-0.5">
{availableD4Groups.map((group) => {
const active = d4PlayGroup === group.id;
return (
<button
key={group.id}
type="button"
onClick={() => setD4PlayGroup(group.id)}
className={cn(
"shrink-0 rounded-full px-3 py-1.5 text-xs font-bold transition-colors",
active
? "bg-[#07459f] text-white"
: "border border-[#dce7f7] bg-white text-[#32518d] hover:bg-[#f4f7fb]",
)}
>
{t(group.labelKey)}
</button>
);
})}
</div>
) : null}
{jackpot?.enabled ? (
<div className="rounded-xl border border-amber-200 bg-gradient-to-r from-amber-50 via-white to-[#f8fbff] px-3 py-2.5">
<div className="flex flex-wrap items-center justify-between gap-2">