|
|
|
|
@@ -10,6 +10,7 @@ import { getPlayEffective } from "@/api/play";
|
|
|
|
|
import { getWalletBalance } from "@/api/wallet";
|
|
|
|
|
import { postTicketPlace, postTicketPreview } from "@/api/ticket";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
|
import { isHallSealedCountdownUi } from "@/features/draw/draw-status-meta";
|
|
|
|
|
@@ -29,7 +30,11 @@ import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
|
|
|
|
import { triggerWalletPollingAfterBet } from "@/hooks/use-wallet-polling";
|
|
|
|
|
import { usePlayerSessionStore } from "@/stores/player-session-store";
|
|
|
|
|
import { getLotteryEcho } from "@/lib/lottery-echo";
|
|
|
|
|
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
|
|
|
|
|
import {
|
|
|
|
|
formatMinorAmount,
|
|
|
|
|
formatMinorAsCurrency,
|
|
|
|
|
parseDecimalInputToMinor,
|
|
|
|
|
} from "@/lib/money";
|
|
|
|
|
import { playLabel } from "@/lib/play-labels";
|
|
|
|
|
import { playerViewportFixedBarClass } from "@/lib/player-viewport";
|
|
|
|
|
import {
|
|
|
|
|
@@ -48,10 +53,18 @@ import type { BetProviderRow } from "@/types/api/bet-provider";
|
|
|
|
|
type HallCategory = "D2" | "D3" | "D4" | "JACKPOT";
|
|
|
|
|
type PlayHallCategory = Exclude<HallCategory, "JACKPOT">;
|
|
|
|
|
|
|
|
|
|
const TRADITIONAL_PLAY_CODES: Record<PlayHallCategory, readonly string[]> = {
|
|
|
|
|
D4: ["big", "small", "pos_4a", "pos_4b", "pos_4c", "pos_4d", "pos_4e", "four_any", "four_top", "four_lower", "five_d", "six_d"],
|
|
|
|
|
D3: ["pos_3a", "pos_3lower", "pos_3b", "pos_3c", "pos_3d", "pos_3e"],
|
|
|
|
|
D2: ["pos_2a", "pos_2b", "pos_2c", "pos_2d", "pos_2e", "pos_2any"],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DraftRow = {
|
|
|
|
|
id: string;
|
|
|
|
|
number: string;
|
|
|
|
|
amounts: Record<string, string>;
|
|
|
|
|
providerCodes: string[];
|
|
|
|
|
selectionType: "straight" | "reverse" | "full_cover" | "full_play" | "half_play";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DraftEntry = {
|
|
|
|
|
@@ -116,10 +129,22 @@ type CellRiskState = "open" | "warning" | "sold_out";
|
|
|
|
|
type QuickFillState = Record<HallCategory, { favorites: string[]; history: string[] }>;
|
|
|
|
|
|
|
|
|
|
const FALLBACK_BET_PROVIDERS: BetProviderRow[] = [
|
|
|
|
|
{ code: "SG", name: "Singapore", sort_order: 10, is_default: true },
|
|
|
|
|
{ code: "MY", name: "Malaysia", sort_order: 20, is_default: false },
|
|
|
|
|
{ code: "TH", name: "Thailand", sort_order: 30, is_default: false },
|
|
|
|
|
{ code: "SG", name: "Singapore", short_code: "S", sort_order: 10, is_default: true },
|
|
|
|
|
{ code: "MY", name: "Malaysia", short_code: "M", sort_order: 20, is_default: false },
|
|
|
|
|
{ code: "TH", name: "Thailand", short_code: "T", sort_order: 30, is_default: false },
|
|
|
|
|
];
|
|
|
|
|
const DEFAULT_PROVIDER_CODE = "SG";
|
|
|
|
|
const PROVIDER_COLUMN_TONES = [
|
|
|
|
|
"bg-[#fff875] text-slate-950",
|
|
|
|
|
"bg-[#b7d9ff] text-slate-950",
|
|
|
|
|
"bg-[#ffc7ca] text-slate-950",
|
|
|
|
|
"bg-[#d9d8ff] text-slate-950",
|
|
|
|
|
"bg-[#a8f8a4] text-slate-950",
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
function providerColumnTone(index: number): string {
|
|
|
|
|
return PROVIDER_COLUMN_TONES[index % PROVIDER_COLUMN_TONES.length] ?? "bg-slate-100 text-slate-950";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const categoryTabs: { value: PlayHallCategory; label: string }[] = [
|
|
|
|
|
{ value: "D4", label: "4D" },
|
|
|
|
|
@@ -161,7 +186,7 @@ function newDraftRow(): DraftRow {
|
|
|
|
|
typeof crypto !== "undefined" && crypto.randomUUID
|
|
|
|
|
? crypto.randomUUID()
|
|
|
|
|
: `row-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
|
|
|
return { id, number: "", amounts: {} };
|
|
|
|
|
return { id, number: "", amounts: {}, providerCodes: [DEFAULT_PROVIDER_CODE], selectionType: "straight" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPlayOpenForPlayer(row: PlayEffectivePlayRow): boolean {
|
|
|
|
|
@@ -265,6 +290,7 @@ function lineForPlay(
|
|
|
|
|
displayNumber: string,
|
|
|
|
|
amountMinor: number,
|
|
|
|
|
digitSlot?: number,
|
|
|
|
|
selectionType: DraftRow["selectionType"] = "straight",
|
|
|
|
|
): TicketLineInput | null {
|
|
|
|
|
const number = normalizeNumberForPlay(displayNumber, play.play_code);
|
|
|
|
|
if (draftLineIssueReason(play.play_code, displayNumber, digitSlot) !== null) {
|
|
|
|
|
@@ -279,6 +305,7 @@ function lineForPlay(
|
|
|
|
|
number,
|
|
|
|
|
play_code: play.play_code,
|
|
|
|
|
amount: amountMinor,
|
|
|
|
|
selection_type: selectionType,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (playNeedsDimension(play.play_code)) {
|
|
|
|
|
@@ -470,10 +497,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
const [resultData, setResultData] = useState<TicketPlaceData | null>(null);
|
|
|
|
|
const [activeCategory, setActiveCategory] = useState<PlayHallCategory>("D4");
|
|
|
|
|
const [betProviders, setBetProviders] = useState<BetProviderRow[]>(FALLBACK_BET_PROVIDERS);
|
|
|
|
|
const [selectedProviderCodes, setSelectedProviderCodes] = useState<string[]>([]);
|
|
|
|
|
const [syncAmountColumns, setSyncAmountColumns] = useState<Record<string, boolean>>({});
|
|
|
|
|
const [quickFillState, setQuickFillState] = useState<QuickFillState>(() => loadQuickFillState());
|
|
|
|
|
const [quickFillExpanded, setQuickFillExpanded] = useState(false);
|
|
|
|
|
const [providersExpanded, setProvidersExpanded] = useState(false);
|
|
|
|
|
const [riskStateDrawNo, setRiskStateDrawNo] = useState<string | null>(display?.draw_no ?? null);
|
|
|
|
|
const [liveSoldOutNumbers, setLiveSoldOutNumbers] = useState<Set<string>>(() => new Set());
|
|
|
|
|
const [liveWarningNumbers, setLiveWarningNumbers] = useState<Set<string>>(() => new Set());
|
|
|
|
|
@@ -497,7 +523,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
const catalogSeqRef = useRef(0);
|
|
|
|
|
const walletSeqRef = useRef(0);
|
|
|
|
|
const selectedCatalogProviderCode =
|
|
|
|
|
selectedProviderCodes[0] ??
|
|
|
|
|
betProviders.find((provider) => provider.is_default)?.code ??
|
|
|
|
|
betProviders[0]?.code;
|
|
|
|
|
|
|
|
|
|
@@ -543,9 +568,16 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
if (cancelled) return;
|
|
|
|
|
const items = data.items.length > 0 ? data.items : FALLBACK_BET_PROVIDERS;
|
|
|
|
|
setBetProviders(items);
|
|
|
|
|
setSelectedProviderCodes((current) => {
|
|
|
|
|
setRows((current) => {
|
|
|
|
|
const available = new Set(items.map((item) => item.code));
|
|
|
|
|
return current.filter((code) => available.has(code));
|
|
|
|
|
const fallback = items.find((item) => item.is_default)?.code ?? items[0]?.code;
|
|
|
|
|
return current.map((row) => {
|
|
|
|
|
const selected = row.providerCodes.filter((code) => available.has(code));
|
|
|
|
|
const wasInitialDefault = row.providerCodes.length === 1 && row.providerCodes[0] === DEFAULT_PROVIDER_CODE;
|
|
|
|
|
return selected.length === 0 && wasInitialDefault && fallback
|
|
|
|
|
? { ...row, providerCodes: [fallback] }
|
|
|
|
|
: { ...row, providerCodes: selected };
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
|
|
|
|
@@ -586,7 +618,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
}, [catalogState]);
|
|
|
|
|
|
|
|
|
|
const activeCategoryPlays = useMemo(
|
|
|
|
|
() => openPlays.filter((play) => playCategory(play.play_code) === activeCategory),
|
|
|
|
|
() => openPlays.filter((play) => TRADITIONAL_PLAY_CODES[activeCategory].includes(play.play_code)),
|
|
|
|
|
[activeCategory, openPlays],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -613,22 +645,34 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
|
|
|
|
|
const mobileIndexColClass = "w-[1.75rem] min-w-[1.75rem]";
|
|
|
|
|
const mobileNumberColClass = "w-[4.25rem] min-w-[4.25rem]";
|
|
|
|
|
const mobileSelectionTypeColClass = "w-[4rem] min-w-[4rem]";
|
|
|
|
|
const desktopIndexColClass = "w-[2.15rem] min-w-[2.15rem]";
|
|
|
|
|
const desktopNumberColClass = "w-[5.5rem] min-w-[5.5rem]";
|
|
|
|
|
const desktopSelectionTypeColClass = "w-[4.75rem] min-w-[4.75rem]";
|
|
|
|
|
const indexColClass = isMobile ? mobileIndexColClass : desktopIndexColClass;
|
|
|
|
|
const numberColClass = isMobile ? mobileNumberColClass : desktopNumberColClass;
|
|
|
|
|
const selectionTypeColClass = isMobile ? mobileSelectionTypeColClass : desktopSelectionTypeColClass;
|
|
|
|
|
const showSelectionTypeColumn = activeCategory !== "D2";
|
|
|
|
|
const stickyNumberLeftClass = isMobile ? "left-[1.75rem]" : "left-[2.15rem]";
|
|
|
|
|
const amountColClass = !isMobile
|
|
|
|
|
? "w-[3.9rem] min-w-[3.9rem]"
|
|
|
|
|
: "w-[4rem] min-w-[4rem]";
|
|
|
|
|
const providerColClass = !isMobile
|
|
|
|
|
? "w-[2.85rem] min-w-[2.85rem]"
|
|
|
|
|
: "w-[2.7rem] min-w-[2.7rem]";
|
|
|
|
|
const rowTotalColClass = !isMobile
|
|
|
|
|
? "w-[5.5rem] min-w-[5.5rem]"
|
|
|
|
|
: "w-[5rem] min-w-[5rem]";
|
|
|
|
|
|
|
|
|
|
const tableMinWidthPx = useMemo(() => {
|
|
|
|
|
const tableWidthPx = useMemo(() => {
|
|
|
|
|
const indexCol = !isMobile ? 34 : 28;
|
|
|
|
|
const numberCol = !isMobile ? 88 : 68;
|
|
|
|
|
const selectionTypeCol = !isMobile ? 76 : 64;
|
|
|
|
|
const providerCol = !isMobile ? 46 : 43;
|
|
|
|
|
const rowTotalCol = !isMobile ? 88 : 80;
|
|
|
|
|
const amountCol = !isMobile ? 62 : 64;
|
|
|
|
|
const deleteCol = 32;
|
|
|
|
|
return indexCol + numberCol + playColumns.length * amountCol + deleteCol;
|
|
|
|
|
}, [playColumns.length, isMobile]);
|
|
|
|
|
return indexCol + numberCol + (showSelectionTypeColumn ? selectionTypeCol : 0) + betProviders.length * providerCol + rowTotalCol + playColumns.length * amountCol;
|
|
|
|
|
}, [betProviders.length, isMobile, playColumns.length, showSelectionTypeColumn]);
|
|
|
|
|
|
|
|
|
|
const showWideTableHint = isMobile && playColumns.length > 8;
|
|
|
|
|
|
|
|
|
|
@@ -668,28 +712,45 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
setActiveRowId(id);
|
|
|
|
|
}, [activeCategory]);
|
|
|
|
|
|
|
|
|
|
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
|
|
|
|
|
setRows((current) =>
|
|
|
|
|
current.map((row) =>
|
|
|
|
|
row.id === rowId
|
|
|
|
|
? { ...row, amounts: { ...row.amounts, [playCode]: sanitizeAmount(value) } }
|
|
|
|
|
: row,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
setActiveRowId(rowId);
|
|
|
|
|
const updateRowSelectionType = useCallback((id: string, selectionType: DraftRow["selectionType"]) => {
|
|
|
|
|
setRows((current) => current.map((row) => row.id === id ? { ...row, selectionType } : row));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const removeRow = useCallback((id: string) => {
|
|
|
|
|
setRows((current) => {
|
|
|
|
|
if (current.length <= 1) return current;
|
|
|
|
|
return current.filter((row) => row.id !== id);
|
|
|
|
|
});
|
|
|
|
|
setActiveRowId((prev) => {
|
|
|
|
|
if (prev === id) return null;
|
|
|
|
|
return prev;
|
|
|
|
|
});
|
|
|
|
|
const updateAllSelectionTypes = useCallback((selectionType: DraftRow["selectionType"]) => {
|
|
|
|
|
setRows((current) => current.map((row) => ({ ...row, selectionType })));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const updateAmount = useCallback((rowId: string, playCode: string, value: string) => {
|
|
|
|
|
const amount = sanitizeAmount(value);
|
|
|
|
|
const column = allPlayColumns.find((item) => item.key === playCode);
|
|
|
|
|
const syncColumn = syncAmountColumns[playCode] === true && column !== undefined;
|
|
|
|
|
|
|
|
|
|
setRows((current) => current.map((row) => {
|
|
|
|
|
if (!syncColumn) {
|
|
|
|
|
return row.id === rowId
|
|
|
|
|
? { ...row, amounts: { ...row.amounts, [playCode]: amount } }
|
|
|
|
|
: row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validNumber = draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) === null;
|
|
|
|
|
const status = cellRiskState(
|
|
|
|
|
column.play,
|
|
|
|
|
row.number,
|
|
|
|
|
playCategory(column.play.play_code),
|
|
|
|
|
alertRows,
|
|
|
|
|
liveSoldOutNumbers,
|
|
|
|
|
liveWarningNumbers,
|
|
|
|
|
column.digitSlot,
|
|
|
|
|
);
|
|
|
|
|
if (!validNumber || status === "sold_out" || (column.play.config !== null && !column.play.config.is_enabled)) {
|
|
|
|
|
return row;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ...row, amounts: { ...row.amounts, [playCode]: amount } };
|
|
|
|
|
}));
|
|
|
|
|
setActiveRowId(rowId);
|
|
|
|
|
}, [alertRows, allPlayColumns, liveSoldOutNumbers, liveWarningNumbers, syncAmountColumns]);
|
|
|
|
|
|
|
|
|
|
const clearAllRows = () => {
|
|
|
|
|
if (tableDisabled) return;
|
|
|
|
|
const next = [newDraftRow()];
|
|
|
|
|
@@ -786,6 +847,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
...row,
|
|
|
|
|
number: previousRow.number,
|
|
|
|
|
amounts: nextAmounts,
|
|
|
|
|
providerCodes: previousRow.providerCodes,
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
@@ -826,14 +888,23 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleProvider = (code: string) => {
|
|
|
|
|
setSelectedProviderCodes((current) => {
|
|
|
|
|
if (current.includes(code)) {
|
|
|
|
|
return current.filter((item) => item !== code);
|
|
|
|
|
}
|
|
|
|
|
return [...current, code];
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const toggleRowProvider = useCallback((rowId: string, code: string) => {
|
|
|
|
|
setRows((current) => current.map((row) => {
|
|
|
|
|
if (row.id !== rowId) return row;
|
|
|
|
|
return row.providerCodes.includes(code)
|
|
|
|
|
? { ...row, providerCodes: row.providerCodes.filter((item) => item !== code) }
|
|
|
|
|
: { ...row, providerCodes: [...row.providerCodes, code] };
|
|
|
|
|
}));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const toggleProviderColumn = useCallback((code: string, checked: boolean) => {
|
|
|
|
|
setRows((current) => current.map((row) => {
|
|
|
|
|
const hasCode = row.providerCodes.includes(code);
|
|
|
|
|
if (checked && !hasCode) return { ...row, providerCodes: [...row.providerCodes, code] };
|
|
|
|
|
if (!checked && hasCode) return { ...row, providerCodes: row.providerCodes.filter((item) => item !== code) };
|
|
|
|
|
return row;
|
|
|
|
|
}));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const hasAmountsForPlay = useCallback(
|
|
|
|
|
(playCode: string): boolean =>
|
|
|
|
|
@@ -958,6 +1029,17 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
return issues;
|
|
|
|
|
}, [allPlayColumns, currencyCode, rows]);
|
|
|
|
|
|
|
|
|
|
const providerMissingRow = useCallback((): number | null => {
|
|
|
|
|
for (const [index, row] of rows.entries()) {
|
|
|
|
|
const hasAmount = Object.values(row.amounts).some((value) => {
|
|
|
|
|
const amount = parseDecimalInputToMinor(value, currencyCode);
|
|
|
|
|
return amount !== null && amount > 0;
|
|
|
|
|
});
|
|
|
|
|
if (hasAmount && row.providerCodes.length === 0) return index + 1;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}, [currencyCode, rows]);
|
|
|
|
|
|
|
|
|
|
const formatDraftLineIssue = useCallback(
|
|
|
|
|
(issue: DraftLineIssue): string => {
|
|
|
|
|
const label = playLabel(issue.playCode, t);
|
|
|
|
|
@@ -972,8 +1054,9 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
allPlayColumns.forEach((column) => {
|
|
|
|
|
const amount = parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode);
|
|
|
|
|
if (amount === null || amount <= 0) return;
|
|
|
|
|
const line = lineForPlay(column.play, row.number, amount, column.digitSlot);
|
|
|
|
|
const line = lineForPlay(column.play, row.number, amount, column.digitSlot, row.selectionType);
|
|
|
|
|
if (!line) return;
|
|
|
|
|
line.provider_codes = row.providerCodes;
|
|
|
|
|
entries.push({
|
|
|
|
|
rowId: row.id,
|
|
|
|
|
rowNo: rowIndex + 1,
|
|
|
|
|
@@ -990,25 +1073,20 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
}, [allPlayColumns, currencyCode, rows]);
|
|
|
|
|
|
|
|
|
|
const draftEntries = collectEntries();
|
|
|
|
|
const effectiveProviderCount = selectedProviderCodes.length > 0 ? selectedProviderCodes.length : 1;
|
|
|
|
|
const draftSummary = useMemo(() => {
|
|
|
|
|
const base = draftEntries.reduce(
|
|
|
|
|
return draftEntries.reduce(
|
|
|
|
|
(acc, entry) => {
|
|
|
|
|
const rebateRate = parseRebateRate(entry.play.odds?.rebate_rate);
|
|
|
|
|
const rebate = Math.round(entry.amountMinor * rebateRate);
|
|
|
|
|
acc.bet += entry.amountMinor;
|
|
|
|
|
acc.rebate += rebate;
|
|
|
|
|
acc.actual += Math.max(0, entry.amountMinor - rebate);
|
|
|
|
|
const providerCount = entry.line.provider_codes?.length ?? 0;
|
|
|
|
|
acc.bet += entry.amountMinor * providerCount;
|
|
|
|
|
acc.rebate += rebate * providerCount;
|
|
|
|
|
acc.actual += Math.max(0, entry.amountMinor - rebate) * providerCount;
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{ bet: 0, rebate: 0, actual: 0 },
|
|
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
bet: base.bet * effectiveProviderCount,
|
|
|
|
|
rebate: base.rebate * effectiveProviderCount,
|
|
|
|
|
actual: base.actual * effectiveProviderCount,
|
|
|
|
|
};
|
|
|
|
|
}, [draftEntries, effectiveProviderCount]);
|
|
|
|
|
}, [draftEntries]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const id = window.setTimeout(() => {
|
|
|
|
|
@@ -1068,6 +1146,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lineIssues = collectDraftLineIssues();
|
|
|
|
|
const missingProviderAt = providerMissingRow();
|
|
|
|
|
if (missingProviderAt !== null) {
|
|
|
|
|
toast.error(t("hall.providers.rowRequired", { row: missingProviderAt, defaultValue: `第 ${missingProviderAt} 行请选择至少一个开注商` }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (lineIssues.length > 0) {
|
|
|
|
|
toast.error(formatDraftLineIssue(lineIssues[0]));
|
|
|
|
|
return;
|
|
|
|
|
@@ -1090,7 +1173,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
draw_id: display.draw_no,
|
|
|
|
|
currency_code: currencyCode,
|
|
|
|
|
client_trace_id: placeTraceIdRef.current,
|
|
|
|
|
provider_codes: selectedProviderCodes.length > 0 ? selectedProviderCodes : undefined,
|
|
|
|
|
lines,
|
|
|
|
|
});
|
|
|
|
|
setPreviewData(data);
|
|
|
|
|
@@ -1131,6 +1213,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lineIssues = collectDraftLineIssues();
|
|
|
|
|
const missingProviderAt = providerMissingRow();
|
|
|
|
|
if (missingProviderAt !== null) {
|
|
|
|
|
toast.error(t("hall.providers.rowRequired", { row: missingProviderAt, defaultValue: `第 ${missingProviderAt} 行请选择至少一个开注商` }));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (lineIssues.length > 0) {
|
|
|
|
|
toast.error(formatDraftLineIssue(lineIssues[0]));
|
|
|
|
|
return;
|
|
|
|
|
@@ -1158,7 +1245,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
draw_id: drawIdForBet,
|
|
|
|
|
currency_code: currencyCode,
|
|
|
|
|
client_trace_id: traceId,
|
|
|
|
|
provider_codes: selectedProviderCodes.length > 0 ? selectedProviderCodes : undefined,
|
|
|
|
|
lines,
|
|
|
|
|
expected_config_versions: previewData.config_versions,
|
|
|
|
|
});
|
|
|
|
|
@@ -1258,27 +1344,11 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
|
|
|
|
|
const submitActualMinor =
|
|
|
|
|
previewData?.summary.total_actual_deduct ?? debouncedSummary.actual;
|
|
|
|
|
const selectedProviderCount = new Set(rows.flatMap((row) => row.providerCodes)).size;
|
|
|
|
|
const canSubmit =
|
|
|
|
|
!tableDisabled && draftEntries.length > 0 && availableMinor >= submitActualMinor;
|
|
|
|
|
!tableDisabled && draftEntries.length > 0 && providerMissingRow() === null && availableMinor >= submitActualMinor;
|
|
|
|
|
const favoriteChips = favorites.slice(0, 10);
|
|
|
|
|
const historyChips = historyNumbers.slice(0, 20);
|
|
|
|
|
const selectedProviderNames = betProviders
|
|
|
|
|
.filter((provider) => selectedProviderCodes.includes(provider.code))
|
|
|
|
|
.map((provider) => provider.name);
|
|
|
|
|
const implicitDefaultProvider =
|
|
|
|
|
betProviders.find((provider) => provider.is_default) ?? betProviders[0] ?? null;
|
|
|
|
|
const providerSummaryCount = selectedProviderCodes.length > 0 ? selectedProviderCodes.length : 1;
|
|
|
|
|
const providerSummaryText =
|
|
|
|
|
selectedProviderCodes.length > 0
|
|
|
|
|
? t("hall.providers.summary", {
|
|
|
|
|
count: providerSummaryCount,
|
|
|
|
|
})
|
|
|
|
|
: implicitDefaultProvider
|
|
|
|
|
? t("hall.providers.platformDefaultSummary", {
|
|
|
|
|
provider: implicitDefaultProvider.name,
|
|
|
|
|
})
|
|
|
|
|
: t("hall.providers.platformDefaultSummaryNoProvider");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<section className="space-y-3" aria-label={t("hall.aria")}>
|
|
|
|
|
@@ -1515,107 +1585,6 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-lg border border-[#dfe8f6] bg-[#f8fbff]",
|
|
|
|
|
isMobile ? "px-3 py-2.5" : "px-2.5 py-2",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
|
|
|
<span className="mr-1 text-[11px] font-black text-[#304f86]">
|
|
|
|
|
{t("hall.providers.title")}
|
|
|
|
|
</span>
|
|
|
|
|
{!isMobile ? betProviders.map((provider) => {
|
|
|
|
|
const checked = selectedProviderCodes.includes(provider.code);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={provider.code}
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
aria-pressed={checked}
|
|
|
|
|
onClick={() => toggleProvider(provider.code)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-flex h-8 items-center gap-1 rounded-md border px-2 text-xs font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
|
|
|
checked
|
|
|
|
|
? "border-[#2d63e2] bg-[#2d63e2] text-white"
|
|
|
|
|
: "border-[#cfdbee] bg-white text-[#304f86] hover:border-[#8fb0eb]",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 rounded-[3px] border",
|
|
|
|
|
checked ? "border-white bg-white/25" : "border-[#9aa8bd]",
|
|
|
|
|
)}
|
|
|
|
|
aria-hidden
|
|
|
|
|
/>
|
|
|
|
|
{provider.name}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}) : (
|
|
|
|
|
<span className="text-xs font-semibold text-[#58709d]">
|
|
|
|
|
{selectedProviderNames.length > 0
|
|
|
|
|
? selectedProviderNames.join(" / ")
|
|
|
|
|
: implicitDefaultProvider
|
|
|
|
|
? t("hall.providers.platformDefault", { provider: implicitDefaultProvider.name })
|
|
|
|
|
: t("hall.providers.platformDefaultNoProvider")}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<p className="text-xs font-semibold text-[#58709d]">{providerSummaryText}</p>
|
|
|
|
|
{isMobile ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
aria-pressed={providersExpanded}
|
|
|
|
|
onClick={() => setProvidersExpanded((current) => !current)}
|
|
|
|
|
className="inline-flex h-7 items-center justify-center rounded-full border border-[#d4dff1] bg-white px-2 text-[#58709d] transition-colors hover:bg-[#f3f7ff] disabled:opacity-40"
|
|
|
|
|
>
|
|
|
|
|
{providersExpanded ? (
|
|
|
|
|
<ChevronUp className="size-3.5" aria-hidden />
|
|
|
|
|
) : (
|
|
|
|
|
<ChevronDown className="size-3.5" aria-hidden />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{isMobile && providersExpanded ? (
|
|
|
|
|
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
|
|
|
|
{betProviders.map((provider) => {
|
|
|
|
|
const checked = selectedProviderCodes.includes(provider.code);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={provider.code}
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
aria-pressed={checked}
|
|
|
|
|
onClick={() => toggleProvider(provider.code)}
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-flex h-8 items-center gap-1 rounded-md border px-2 text-xs font-bold transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
|
|
|
|
checked
|
|
|
|
|
? "border-[#2d63e2] bg-[#2d63e2] text-white"
|
|
|
|
|
: "border-[#cfdbee] bg-white text-[#304f86] hover:border-[#8fb0eb]",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"size-3 rounded-[3px] border",
|
|
|
|
|
checked ? "border-white bg-white/25" : "border-[#9aa8bd]",
|
|
|
|
|
)}
|
|
|
|
|
aria-hidden
|
|
|
|
|
/>
|
|
|
|
|
{provider.name}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className={cn("overflow-x-auto pb-1", isMobile ? "-mx-1 flex gap-1.5 px-1" : "flex items-end gap-2")}>
|
|
|
|
|
{categoryTabs.map((tab) => {
|
|
|
|
|
const hasPlays = openPlays.some(
|
|
|
|
|
@@ -1689,7 +1658,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
<tbody className="bg-white text-[#17408d]">
|
|
|
|
|
<tr>
|
|
|
|
|
<td className="border-r border-[#edf2f8] px-2 py-1.5 font-bold text-[#e5002c]">
|
|
|
|
|
{formatMinorAsCurrency(submitActualMinor, currencyCode)}
|
|
|
|
|
{formatMinorAmount(submitActualMinor)}
|
|
|
|
|
</td>
|
|
|
|
|
{allPlayColumns.map((column) => {
|
|
|
|
|
const colTotalMinor = rows.reduce((sum, row) => {
|
|
|
|
|
@@ -1707,7 +1676,7 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<td key={`val-${column.key}`} className="border-r border-[#edf2f8] px-1 py-1.5 font-bold last:border-0">
|
|
|
|
|
{formatMinorAsCurrency(colTotalMinor, currencyCode)}
|
|
|
|
|
{formatMinorAmount(colTotalMinor)}
|
|
|
|
|
</td>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
@@ -1716,10 +1685,10 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="player-table-scroll overflow-x-auto overscroll-x-contain">
|
|
|
|
|
<div className="player-table-scroll flex overflow-x-auto overscroll-x-contain">
|
|
|
|
|
<table
|
|
|
|
|
className="w-full border-collapse text-[11px]"
|
|
|
|
|
style={{ minWidth: tableMinWidthPx }}
|
|
|
|
|
className="w-max border-collapse text-[11px]"
|
|
|
|
|
style={{ width: tableWidthPx }}
|
|
|
|
|
>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-[#edf2f8] bg-[#f8fafd] text-[#58709d]">
|
|
|
|
|
@@ -1751,12 +1720,65 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
t,
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="mt-0.5 block text-[9px] font-medium text-[#9aa8bd]">
|
|
|
|
|
{t("hall.table.amountPlaceholder")}
|
|
|
|
|
</span>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={syncAmountColumns[column.key] === true}
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
onCheckedChange={(checked) => setSyncAmountColumns((current) => ({
|
|
|
|
|
...current,
|
|
|
|
|
[column.key]: checked === true,
|
|
|
|
|
}))}
|
|
|
|
|
aria-label={`${playColumnHeaderLabel(column.play, playCategory(column.play.play_code), column.digitSlot, t)} 同步本列金额`}
|
|
|
|
|
title="同步本列金额"
|
|
|
|
|
className="mx-auto mt-1 size-3.5"
|
|
|
|
|
/>
|
|
|
|
|
</th>
|
|
|
|
|
))}
|
|
|
|
|
<th className="w-8 min-w-8 px-0.5 py-1.5" aria-label={t("hall.table.delete")} />
|
|
|
|
|
{showSelectionTypeColumn ? (
|
|
|
|
|
<th className={cn(selectionTypeColClass, "px-1 py-1 text-center font-bold")}>
|
|
|
|
|
<span className="block text-xs">{t("hall.table.selectionType", { defaultValue: "Type" })}</span>
|
|
|
|
|
<select
|
|
|
|
|
defaultValue=""
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
const selectionType = event.target.value as DraftRow["selectionType"] | "";
|
|
|
|
|
if (selectionType) updateAllSelectionTypes(selectionType);
|
|
|
|
|
event.currentTarget.value = "";
|
|
|
|
|
}}
|
|
|
|
|
className="mx-auto mt-1 h-5 w-full rounded border border-[#d7e1f3] bg-white px-0.5 text-[10px] font-semibold text-[#304f86]"
|
|
|
|
|
aria-label="全选种类"
|
|
|
|
|
title="统一设置所有行的种类"
|
|
|
|
|
>
|
|
|
|
|
<option value="">全选</option>
|
|
|
|
|
<option value="straight">正字</option>
|
|
|
|
|
<option value="reverse">来回</option>
|
|
|
|
|
<option value="full_cover">全保</option>
|
|
|
|
|
<option value="full_play">全打</option>
|
|
|
|
|
{activeCategory === "D4" ? <option value="half_play">半打</option> : null}
|
|
|
|
|
</select>
|
|
|
|
|
</th>
|
|
|
|
|
) : null}
|
|
|
|
|
{betProviders.map((provider, providerIndex) => {
|
|
|
|
|
const checked = rows.length > 0 && rows.every((row) => row.providerCodes.includes(provider.code));
|
|
|
|
|
return (
|
|
|
|
|
<th
|
|
|
|
|
key={provider.code}
|
|
|
|
|
className={cn(providerColClass, providerColumnTone(providerIndex), "px-0.5 py-1 text-center font-black")}
|
|
|
|
|
title={provider.name}
|
|
|
|
|
>
|
|
|
|
|
<span className="block text-sm leading-none">{provider.short_code}</span>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={checked}
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
onCheckedChange={(next) => toggleProviderColumn(provider.code, next === true)}
|
|
|
|
|
aria-label={`全选 ${provider.name}`}
|
|
|
|
|
className="mx-auto mt-1 border-slate-500 bg-white data-checked:border-slate-800 data-checked:bg-slate-800"
|
|
|
|
|
/>
|
|
|
|
|
</th>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
<th className={cn(rowTotalColClass, "bg-[#eef5ff] px-1 py-1 text-center font-black text-[#17408d]")}>
|
|
|
|
|
<span className="block text-xs">{t("hall.table.rowTotal", { defaultValue: "Total" })}</span>
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
@@ -1770,24 +1792,51 @@ export function HallBettingGrid({ drawLive }: { drawLive: HallDrawLiveSnapshot }
|
|
|
|
|
numberPlaceholder={numberPlaceholder}
|
|
|
|
|
activeCategory={activeCategory}
|
|
|
|
|
numberMaxChars={numberMaxChars}
|
|
|
|
|
betProviders={betProviders}
|
|
|
|
|
playColumns={playColumns}
|
|
|
|
|
alertRows={alertRows}
|
|
|
|
|
liveSoldOutNumbers={liveSoldOutNumbers}
|
|
|
|
|
liveWarningNumbers={liveWarningNumbers}
|
|
|
|
|
updateRowNumber={updateRowNumber}
|
|
|
|
|
updateRowSelectionType={updateRowSelectionType}
|
|
|
|
|
updateAmount={updateAmount}
|
|
|
|
|
toggleRowProvider={toggleRowProvider}
|
|
|
|
|
setActiveRowId={setActiveRowId}
|
|
|
|
|
removeRow={removeRow}
|
|
|
|
|
t={t}
|
|
|
|
|
isMobile={isMobile}
|
|
|
|
|
removable={rows.length > 1}
|
|
|
|
|
indexColClass={indexColClass}
|
|
|
|
|
numberColClass={numberColClass}
|
|
|
|
|
stickyNumberLeftClass={stickyNumberLeftClass}
|
|
|
|
|
selectionTypeColClass={selectionTypeColClass}
|
|
|
|
|
showSelectionTypeColumn={showSelectionTypeColumn}
|
|
|
|
|
providerColClass={providerColClass}
|
|
|
|
|
rowTotalColClass={rowTotalColClass}
|
|
|
|
|
currencyCode={currencyCode}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
{!isMobile ? (
|
|
|
|
|
<aside className="min-w-[12rem] flex-1 border-l border-[#e6edf8] bg-[#f8fbff] px-4 py-4 text-[#304f86]">
|
|
|
|
|
<p className="text-xs font-black">{t("hall.table.orderSummary", { defaultValue: "Order summary" })}</p>
|
|
|
|
|
<dl className="mt-5 space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<dt className="text-xs text-slate-500">{t("hall.table.lineCount", { defaultValue: "Lines" })}</dt>
|
|
|
|
|
<dd className="font-mono text-base font-black tabular-nums">{draftEntries.length}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<dt className="text-xs text-slate-500">{t("hall.table.providerCount", { defaultValue: "Providers" })}</dt>
|
|
|
|
|
<dd className="font-mono text-base font-black tabular-nums">{selectedProviderCount}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="border-t border-[#dce6f5] pt-4">
|
|
|
|
|
<dt className="text-xs text-slate-500">{t("hall.table.actualTotal")}</dt>
|
|
|
|
|
<dd className="mt-1 text-lg font-black tabular-nums text-[#0b3f96]">
|
|
|
|
|
{formatMinorAsCurrency(submitActualMinor, currencyCode)}
|
|
|
|
|
</dd>
|
|
|
|
|
</div>
|
|
|
|
|
</dl>
|
|
|
|
|
</aside>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
@@ -1925,20 +1974,26 @@ const DraftRowItem = memo(function DraftRowItem({
|
|
|
|
|
numberPlaceholder,
|
|
|
|
|
activeCategory,
|
|
|
|
|
numberMaxChars,
|
|
|
|
|
betProviders,
|
|
|
|
|
playColumns,
|
|
|
|
|
alertRows,
|
|
|
|
|
liveSoldOutNumbers,
|
|
|
|
|
liveWarningNumbers,
|
|
|
|
|
updateRowNumber,
|
|
|
|
|
updateRowSelectionType,
|
|
|
|
|
updateAmount,
|
|
|
|
|
toggleRowProvider,
|
|
|
|
|
setActiveRowId,
|
|
|
|
|
removeRow,
|
|
|
|
|
t,
|
|
|
|
|
isMobile,
|
|
|
|
|
removable,
|
|
|
|
|
indexColClass,
|
|
|
|
|
numberColClass,
|
|
|
|
|
stickyNumberLeftClass,
|
|
|
|
|
selectionTypeColClass,
|
|
|
|
|
showSelectionTypeColumn,
|
|
|
|
|
providerColClass,
|
|
|
|
|
rowTotalColClass,
|
|
|
|
|
currencyCode,
|
|
|
|
|
}: {
|
|
|
|
|
row: DraftRow;
|
|
|
|
|
index: number;
|
|
|
|
|
@@ -1947,22 +2002,32 @@ const DraftRowItem = memo(function DraftRowItem({
|
|
|
|
|
numberPlaceholder: string;
|
|
|
|
|
activeCategory: PlayHallCategory;
|
|
|
|
|
numberMaxChars: number;
|
|
|
|
|
betProviders: BetProviderRow[];
|
|
|
|
|
playColumns: PlayColumn[];
|
|
|
|
|
alertRows: DrawCurrentRiskPoolAlert[];
|
|
|
|
|
liveSoldOutNumbers: Set<string>;
|
|
|
|
|
liveWarningNumbers: Set<string>;
|
|
|
|
|
updateRowNumber: (id: string, value: string) => void;
|
|
|
|
|
updateRowSelectionType: (id: string, value: DraftRow["selectionType"]) => void;
|
|
|
|
|
updateAmount: (rowId: string, playCode: string, value: string) => void;
|
|
|
|
|
toggleRowProvider: (rowId: string, code: string) => void;
|
|
|
|
|
setActiveRowId: (id: string) => void;
|
|
|
|
|
removeRow: (id: string) => void;
|
|
|
|
|
t: HallTranslate;
|
|
|
|
|
isMobile: boolean;
|
|
|
|
|
removable: boolean;
|
|
|
|
|
indexColClass: string;
|
|
|
|
|
numberColClass: string;
|
|
|
|
|
stickyNumberLeftClass: string;
|
|
|
|
|
selectionTypeColClass: string;
|
|
|
|
|
showSelectionTypeColumn: boolean;
|
|
|
|
|
providerColClass: string;
|
|
|
|
|
rowTotalColClass: string;
|
|
|
|
|
currencyCode: string;
|
|
|
|
|
}) {
|
|
|
|
|
const displayNumber = sanitizeNumber(row.number, activeCategory);
|
|
|
|
|
const rowTotalMinor = playColumns.reduce((total, column) => {
|
|
|
|
|
if (draftLineIssueReason(column.play.play_code, row.number, column.digitSlot) !== null) return total;
|
|
|
|
|
return total + (parseDecimalInputToMinor(row.amounts[column.key] ?? "", currencyCode) ?? 0);
|
|
|
|
|
}, 0) * row.providerCodes.length;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<tr
|
|
|
|
|
@@ -1988,7 +2053,7 @@ const DraftRowItem = memo(function DraftRowItem({
|
|
|
|
|
rowActive ? "bg-[#f5f9ff]" : "bg-white",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
<Input
|
|
|
|
|
value={displayNumber}
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
@@ -2068,16 +2133,39 @@ const DraftRowItem = memo(function DraftRowItem({
|
|
|
|
|
</td>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
<td className="px-0.5 py-1.5 text-center align-middle">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
disabled={tableDisabled || !removable}
|
|
|
|
|
onClick={() => removeRow(row.id)}
|
|
|
|
|
className="inline-flex size-7 items-center justify-center rounded-full text-[#e5002c] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
|
|
|
|
|
aria-label={t("actions.deleteRow", { row: index + 1 })}
|
|
|
|
|
{showSelectionTypeColumn ? (
|
|
|
|
|
<td className={cn(selectionTypeColClass, "px-1 py-1.5 text-center", rowActive && "bg-[#f5f9ff]")}>
|
|
|
|
|
<select
|
|
|
|
|
value={row.selectionType}
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
onChange={(event) => updateRowSelectionType(row.id, event.target.value as DraftRow["selectionType"])}
|
|
|
|
|
className="h-8 w-full rounded-md border border-[#e1e8f3] bg-white px-1 text-[10px] font-semibold text-[#304f86]"
|
|
|
|
|
aria-label={`第 ${index + 1} 行种类`}
|
|
|
|
|
>
|
|
|
|
|
<option value="straight">正字</option>
|
|
|
|
|
<option value="reverse">来回</option>
|
|
|
|
|
<option value="full_cover">全保</option>
|
|
|
|
|
<option value="full_play">全打</option>
|
|
|
|
|
{activeCategory === "D4" ? <option value="half_play">半打</option> : null}
|
|
|
|
|
</select>
|
|
|
|
|
</td>
|
|
|
|
|
) : null}
|
|
|
|
|
{betProviders.map((provider, providerIndex) => (
|
|
|
|
|
<td
|
|
|
|
|
key={provider.code}
|
|
|
|
|
className={cn(providerColClass, providerColumnTone(providerIndex), "px-0.5 py-1.5 text-center")}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="size-3.5" aria-hidden />
|
|
|
|
|
</button>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={row.providerCodes.includes(provider.code)}
|
|
|
|
|
disabled={tableDisabled}
|
|
|
|
|
onCheckedChange={() => toggleRowProvider(row.id, provider.code)}
|
|
|
|
|
aria-label={`${provider.name} ${index + 1}`}
|
|
|
|
|
className="mx-auto border-slate-500 bg-white data-checked:border-slate-800 data-checked:bg-slate-800"
|
|
|
|
|
/>
|
|
|
|
|
</td>
|
|
|
|
|
))}
|
|
|
|
|
<td className={cn(rowTotalColClass, "bg-[#f7fbff] px-1 py-1.5 text-center font-mono font-black tabular-nums text-[#0b3f96]", rowActive && "bg-[#edf5ff]")}>
|
|
|
|
|
{formatMinorAmount(rowTotalMinor)}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
|