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

@@ -47,7 +47,7 @@ export function PlayerPanel({
<header <header
className={cn( className={cn(
playerPageHeader, playerPageHeader,
"sticky z-50 grid grid-cols-[minmax(0,38%)_minmax(0,1fr)_minmax(0,48%)] items-center gap-1 bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur", "sticky z-50 grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 bg-white/95 pt-2 pb-2 -mx-3 px-3 backdrop-blur",
)} )}
style={stickyTopStyle} style={stickyTopStyle}
> >

View File

@@ -135,6 +135,23 @@ const D4_PLAY_ORDER = [
"digit_small", "digit_small",
] as const; ] 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[]> = { const categoryPlayOrders: Record<Exclude<HallCategory, "JACKPOT">, readonly string[]> = {
D2: D2_PLAY_ORDER, D2: D2_PLAY_ORDER,
D3: D3_PLAY_ORDER, D3: D3_PLAY_ORDER,
@@ -446,6 +463,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile)); const creditMode = isCreditFundingPlayer(usePlayerSessionStore((state) => state.profile));
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2"); const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
const [d4PlayGroup, setD4PlayGroup] = useState<D4PlayGroupId>("big_small");
const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]); const [rows, setRows] = useState<DraftRow[]>(() => [newDraftRow()]);
const [activeRowId, setActiveRowId] = useState<string | null>(null); const [activeRowId, setActiveRowId] = useState<string | null>(null);
const [catalogState, setCatalogState] = useState< const [catalogState, setCatalogState] = useState<
@@ -557,11 +575,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
); );
}, [activeCategory, catalogState, openPlays]); }, [activeCategory, catalogState, openPlays]);
const playColumns = useMemo(() => { const allPlayColumns = useMemo(() => {
if (activeCategory === "JACKPOT") return []; if (activeCategory === "JACKPOT") return [];
return playColumnsForCategory(categoryPlays, activeCategory); return playColumnsForCategory(categoryPlays, activeCategory);
}, [activeCategory, categoryPlays]); }, [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 tableMinWidthPx = useMemo(() => {
const indexCol = 40; const indexCol = 40;
const numberCol = activeCategory === "D4" ? 112 : activeCategory === "D3" ? 88 : 72; 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; return indexCol + numberCol + playColumns.length * amountCol + deleteCol;
}, [activeCategory, playColumns.length]); }, [activeCategory, playColumns.length]);
const showWideTableHint = activeCategory === "D4" && playColumns.length > 8; const showWideTableHint = activeCategory === "D4" && allPlayColumns.length > 8 && playColumns.length > 8;
const activeRow = useMemo( const activeRow = useMemo(
() => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null, () => rows.find((row) => row.id === activeRowId) ?? rows[0] ?? null,
@@ -598,7 +624,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory !== "D4") return defaultNumberPlaceholder; if (activeCategory !== "D4") return defaultNumberPlaceholder;
const targetRow = activeRow ?? rows[0]; const targetRow = activeRow ?? rows[0];
if (!targetRow) return defaultNumberPlaceholder; if (!targetRow) return defaultNumberPlaceholder;
const hasRollStake = playColumns.some((column) => { const hasRollStake = allPlayColumns.some((column) => {
if (column.play.play_code !== "roll") return false; if (column.play.play_code !== "roll") return false;
const amount = parseDecimalInputToMinor(targetRow.amounts[column.key] ?? "", currencyCode); const amount = parseDecimalInputToMinor(targetRow.amounts[column.key] ?? "", currencyCode);
return amount !== null && amount > 0; return amount !== null && amount > 0;
@@ -609,11 +635,19 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
activeRow, activeRow,
currencyCode, currencyCode,
defaultNumberPlaceholder, defaultNumberPlaceholder,
playColumns, allPlayColumns,
rows, rows,
t, 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) => { const updateRowNumber = (id: string, value: string) => {
setRows((current) => setRows((current) =>
current.map((row) => current.map((row) =>
@@ -809,7 +843,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return []; if (activeCategory === "JACKPOT") return [];
const issues: DraftLineIssue[] = []; const issues: DraftLineIssue[] = [];
rows.forEach((row, rowIndex) => { rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => { allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode); const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return; if (amount === null || amount <= 0) return;
const reason = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot); const reason = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot);
@@ -823,7 +857,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
}); });
}); });
return issues; return issues;
}, [activeCategory, currencyCode, playColumns, rows]); }, [activeCategory, currencyCode, allPlayColumns, rows]);
const formatDraftLineIssue = useCallback( const formatDraftLineIssue = useCallback(
(issue: DraftLineIssue): string => { (issue: DraftLineIssue): string => {
@@ -837,7 +871,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
if (activeCategory === "JACKPOT") return []; if (activeCategory === "JACKPOT") return [];
const entries: DraftEntry[] = []; const entries: DraftEntry[] = [];
rows.forEach((row, rowIndex) => { rows.forEach((row, rowIndex) => {
playColumns.forEach((column) => { allPlayColumns.forEach((column) => {
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode); const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
if (amount === null || amount <= 0) return; if (amount === null || amount <= 0) return;
const line = lineForPlay(activeCategory, column.play, row.number, amount, column.digitSlot); const line = lineForPlay(activeCategory, column.play, row.number, amount, column.digitSlot);
@@ -855,7 +889,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
}); });
}); });
return entries; return entries;
}, [activeCategory, currencyCode, playColumns, rows]); }, [activeCategory, currencyCode, allPlayColumns, rows]);
const draftEntries = collectEntries(); const draftEntries = collectEntries();
const draftSummary = useMemo(() => { const draftSummary = useMemo(() => {
@@ -1155,6 +1189,29 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
</div> </div>
</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 ? ( {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="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"> <div className="flex flex-wrap items-center justify-between gap-2">

View File

@@ -42,7 +42,6 @@ const ORDERS_PAGE_SIZE = 20;
const STATUS_OPTIONS = [ const STATUS_OPTIONS = [
"pending_confirm", "pending_confirm",
"partial_pending_confirm", "partial_pending_confirm",
"placed",
"pending_draw", "pending_draw",
"partial_failed", "partial_failed",
"pending_payout", "pending_payout",

View File

@@ -213,6 +213,14 @@
"boxTitle": "Box", "boxTitle": "Box",
"boxDesc": "Multiply all by amount" "boxDesc": "Multiply all by amount"
}, },
"d4Group": {
"big_small": "Big / Small",
"position": "Position",
"combo": "Combo",
"roll_straight": "Roll / Straight",
"head_tail_odd_even": "Head / Tail / Odd / Even",
"digit_big_small": "Digit Big / Small"
},
"table": { "table": {
"no": "No.", "no": "No.",
"number": "Number", "number": "Number",

View File

@@ -213,6 +213,14 @@
"boxTitle": "Box", "boxTitle": "Box",
"boxDesc": "रकम सबैसँग गुणा गर्नुहोस्" "boxDesc": "रकम सबैसँग गुणा गर्नुहोस्"
}, },
"d4Group": {
"big_small": "ठूलो / सानो",
"position": "स्थान",
"combo": "कम्बो",
"roll_straight": "रोल / स्ट्रेट",
"head_tail_odd_even": "टाउको / पुच्छर / बिजोर / जोर",
"digit_big_small": "डिजिट ठूलो / सानो"
},
"table": { "table": {
"no": "नं.", "no": "नं.",
"number": "नम्बर", "number": "नम्बर",

View File

@@ -211,6 +211,14 @@
"boxTitle": "Box", "boxTitle": "Box",
"boxDesc": "按金额乘以全部组合" "boxDesc": "按金额乘以全部组合"
}, },
"d4Group": {
"big_small": "大小盘",
"position": "位置",
"combo": "组合",
"roll_straight": "滚轮 / 直通",
"head_tail_odd_even": "头尾单双",
"digit_big_small": "各位大小"
},
"table": { "table": {
"no": "序号", "no": "序号",
"number": "号码", "number": "号码",