"use client"; import { Badge, Box, Button, Card, Flex, Grid, Heading, HStack, Icon, IconButton, Separator, SimpleGrid, Spinner, Text, VStack, } from "@chakra-ui/react"; import { useLocale, useMessages, useTranslations } from "next-intl"; import React from "react"; import { LuBadgeAlert, LuCheck, LuCircleHelp, LuDatabase, LuEye, LuEyeOff, LuLayers3, LuListChecks, LuLock, LuRefreshCcw, LuShieldCheck, LuSparkles, LuTarget, LuTrash, LuTrophy, } from "react-icons/lu"; import { SlideUp } from "@/components/motion"; import { useColorModeValue } from "@/components/ui/color-mode"; import { Tooltip } from "@/components/ui/overlays/tooltip"; import FrequencyPanel from "@/components/coupons/frequency-panel"; import { useSuggestCoupon } from "@/lib/api/coupons/use-hooks"; import type { CouponItemDto, SmartCouponResultDto, SuggestedCouponBetDto, } from "@/lib/api/coupons/types"; import { useQueryMatches } from "@/lib/api/matches/use-hooks"; import type { LeagueWithMatchesDto, MatchResponseDto, } from "@/lib/api/matches/types"; import type { CouponStrategy } from "@/lib/api/predictions/types"; import { useCouponStore } from "@/lib/stores/coupon-store"; import { ApiError } from "@/lib/api/create-api-client"; const formatDate = (ts: number | string, locale: string) => new Intl.DateTimeFormat(locale, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }).format(new Date(typeof ts === "string" ? Number(ts) : ts)); const formatPercent = (v?: number, d = 0) => v === undefined || Number.isNaN(v) ? "-" : `${v.toFixed(d)}%`; const formatOdds = (v?: number) => v === undefined || Number.isNaN(v) ? "-" : v.toFixed(2); const strategyPalette = (s: CouponStrategy) => ({ SAFE: "green", BALANCED: "blue", AGGRESSIVE: "orange", VALUE: "red", MIRACLE: "purple", })[s] || "gray"; const riskPalette = (v?: string) => ({ LOW: "green", MEDIUM: "yellow", HIGH: "orange", EXTREME: "red" })[ (v || "").toUpperCase() ] || "gray"; const qualityPalette = (v?: string) => ({ HIGH: "green", MEDIUM: "yellow", LOW: "red" })[(v || "").toUpperCase()] || "gray"; function normalizeSmartCouponResult( value: unknown, ): SmartCouponResultDto | undefined { if (!value || typeof value !== "object") return undefined; let payload = value as Record; while ( payload && typeof payload === "object" && !Array.isArray(payload.bets) && payload.data && typeof payload.data === "object" ) { payload = payload.data as Record; } if (!Array.isArray(payload.bets)) return undefined; return { strategy: String(payload.strategy || "BALANCED") as CouponStrategy, generated_at: String(payload.generated_at || ""), match_count: Number(payload.match_count || 0), bets: payload.bets as SuggestedCouponBetDto[], total_odds: Number(payload.total_odds || 0), expected_win_rate: Number(payload.expected_win_rate || 0), rejected_matches: Array.isArray(payload.rejected_matches) ? (payload.rejected_matches as SmartCouponResultDto["rejected_matches"]) : [], }; } function InfoIcon({ content, label }: { content: string; label: string }) { return ( ); } function StatCard({ label, value, helper, accent, }: { label: string; value: string; helper?: string; accent?: string; }) { const bg = useColorModeValue("white", "gray.900"); const borderColor = useColorModeValue("gray.200", "gray.700"); return ( {label} {helper ? : null} {value} ); } function MatchGroups({ groups, selectable, selectedIds, locale, onToggle, badgeLabel, secondaryBadge, readOnlyLabel, matchStateLabel, finishedReferenceLabel, t, tCommon, }: { groups: LeagueWithMatchesDto[]; selectable: boolean; selectedIds: string[]; locale: string; onToggle: (id: string) => void; badgeLabel: string; secondaryBadge?: string; readOnlyLabel: string; matchStateLabel: string; finishedReferenceLabel: string; t: ReturnType; tCommon: ReturnType; }) { const cardBg = useColorModeValue("white", "gray.800"); const mutedBg = useColorModeValue("gray.50", "whiteAlpha.50"); const borderColor = useColorModeValue("gray.200", "gray.700"); const selectedBorder = useColorModeValue("teal.400", "teal.300"); const subtleGreenBg = useColorModeValue("green.50", "green.950"); return ( {groups.map((league) => ( {league.name} {league.country?.name} • {league.matches.length}{" "} {t("match-count-suffix")} {league.matches.map((match) => { const selected = selectedIds.includes(match.id); return ( onToggle(match.id) : undefined} > {badgeLabel} {selectable && selected ? ( {t("selected-short")} ) : null} {!selectable && secondaryBadge ? ( {secondaryBadge} ) : null} {match.matchName} {selectable ? ( ) : ( {readOnlyLabel} )} {tCommon("date")} {formatDate(match.mstUtc, locale)} {selectable ? t("selection-mode") : matchStateLabel} {selectable ? selected ? t("manual-pool") : t("auto-pool") : finishedReferenceLabel} ); })} ))} ); } export default function CouponBuilderContent() { const t = useTranslations("coupons"); const tCommon = useTranslations("common"); const locale = useLocale(); const messages = useMessages() as { predictions?: { ["market-labels"]?: Record }; }; const marketLabels = messages?.predictions?.["market-labels"]; const copy = React.useCallback( (tr: string, en: string) => (locale === "tr" ? tr : en), [locale], ); const cardBg = useColorModeValue("white", "gray.800"); const mutedBg = useColorModeValue("gray.50", "whiteAlpha.50"); const borderColor = useColorModeValue("gray.200", "gray.700"); const { items, addItem, clearCoupon, removeItem, strategy, setStrategy } = useCouponStore(); const upcomingQuery = useQueryMatches(); const finishedQuery = useQueryMatches(); const suggestCoupon = useSuggestCoupon(); const [activeStrategy, setActiveStrategy] = React.useState( strategy || "BALANCED", ); const [selectedMatchIds, setSelectedMatchIds] = React.useState([]); const [showFinishedMatches, setShowFinishedMatches] = React.useState(false); const [suggestedCoupon, setSuggestedCoupon] = React.useState< SmartCouponResultDto | undefined >(undefined); const [matchCount, setMatchCount] = React.useState(5); // Default: 5 matches const [engineMode, setEngineMode] = React.useState<"ai" | "frequency">("ai"); React.useEffect(() => { if (!upcomingQuery.data && !upcomingQuery.isPending) { upcomingQuery.mutate({ sport: "football", status: "UPCOMING", limit: 40, }); } }, [upcomingQuery.data, upcomingQuery.isPending, upcomingQuery.mutate]); React.useEffect(() => { if ( showFinishedMatches && !finishedQuery.data && !finishedQuery.isPending ) { finishedQuery.mutate({ sport: "football", status: "FINISHED", limit: 20, }); } }, [ finishedQuery.data, finishedQuery.isPending, finishedQuery.mutate, showFinishedMatches, ]); React.useEffect(() => { const coupon = suggestedCoupon; if (!coupon?.bets?.length) return; clearCoupon(); coupon.bets.forEach((bet) => addItem({ matchId: bet.match_id, matchName: bet.match_name, market: bet.market, pick: bet.pick, odd: bet.odds || 0, }), ); }, [addItem, clearCoupon, suggestedCoupon]); const leagueGroups = upcomingQuery.data?.data ?? []; const finishedLeagueGroups = finishedQuery.data?.data ?? []; const allMatches = React.useMemo( () => leagueGroups.flatMap((l) => l.matches), [leagueGroups], ); const allFinishedMatches = React.useMemo( () => finishedLeagueGroups.flatMap((l) => l.matches), [finishedLeagueGroups], ); const selectedMatches = React.useMemo(() => { const set = new Set(selectedMatchIds); return allMatches.filter((m) => set.has(m.id)); }, [allMatches, selectedMatchIds]); const suggestedBets: SuggestedCouponBetDto[] = suggestedCoupon?.bets ?? []; const suggestErrorMessage = suggestCoupon.error instanceof ApiError ? suggestCoupon.error.message : suggestCoupon.error instanceof Error ? suggestCoupon.error.message : undefined; const strategies = [ { key: "SAFE" as CouponStrategy, label: t("strategy-safe"), description: t("strategy-safe-desc"), }, { key: "BALANCED" as CouponStrategy, label: t("strategy-balanced"), description: t("strategy-balanced-desc"), }, { key: "AGGRESSIVE" as CouponStrategy, label: t("strategy-aggressive"), description: t("strategy-aggressive-desc"), }, { key: "VALUE" as CouponStrategy, label: t("strategy-value"), description: t("strategy-value-desc"), }, ]; const toggleMatchSelection = (matchId: string) => setSelectedMatchIds((c) => c.includes(matchId) ? c.filter((id) => id !== matchId) : [...c, matchId], ); const handleSuggest = () => { setStrategy(activeStrategy); // If no matches selected, send all upcoming matches for AI to choose from const matchIdsToSend = selectedMatchIds.length > 0 ? selectedMatchIds : allMatches.map((m) => m.id); // Send all matches from bulletin suggestCoupon.mutate( { matchIds: matchIdsToSend, strategy: activeStrategy, maxMatches: matchCount, // User's desired coupon size }, { onSuccess: (response) => { setSuggestedCoupon(normalizeSmartCouponResult(response)); }, onError: () => { setSuggestedCoupon(undefined); }, }, ); }; const handleRefresh = () => { upcomingQuery.reset(); upcomingQuery.mutate({ sport: "football", status: "UPCOMING", limit: 40 }); if (showFinishedMatches) { finishedQuery.reset(); finishedQuery.mutate({ sport: "football", status: "FINISHED", limit: 20, }); } }; const getStoreItemLabel = (item: CouponItemDto) => `${marketLabels?.[item.market] || item.market}: ${item.pick}`; const finishedMatchCountLabel = copy("Bitmis Mac", "Finished Matches"); const finishedMatchCountHelp = copy( "Istege bagli referans listesi. Bu maclar kupon tahminine asla dahil edilmez.", "Optional reference list of finished football matches. These are never used for coupon prediction.", ); const finishedBadge = copy("Bitti", "Finished"); const predictionLocked = copy("Tahmine Kapali", "Prediction Locked"); const readOnlyShort = copy("Salt okunur", "Read only"); const matchState = copy("Mac Durumu", "Match State"); const finishedReferenceOnly = copy("Sadece referans", "Reference only"); const finishedMatchesTitle = copy("Bitmis Maclar", "Finished Matches"); const finishedMatchesHelp = copy( "Bu maclar sadece referans icin gosterilir. Secilemezler ve backend tarafinda tahmin olusmadan once kesin olarak filtrelenirler.", "These matches are shown only for reference. They cannot be selected and are filtered out on the backend before any coupon prediction is created.", ); const finishedMatchesSubtitle = copy( "Opsiyonel arsiv gorunumu. Skorlar ve mac sonu istatistikleri kupon tahmin akisina hicbir zaman gonderilmez.", "Optional archive view. Scores and post-match stats are never sent into the coupon prediction flow.", ); const showFinishedMatchesLabel = copy( "Bitmis maclari goster", "Show finished matches", ); const hideFinishedMatchesLabel = copy( "Bitmis maclari gizle", "Hide finished matches", ); const noFinishedMatchesLabel = copy( "Bu gorunum icin bitmis futbol maci bulunamadi.", "No finished football matches were found for the current view.", ); return ( {t("builder-title")} {t("builder-subtitle")} 0 ? "teal.500" : undefined} /> 0 ? "green.500" : undefined} /> {t("candidate-pool-title")} {t("candidate-pool-subtitle")} {upcomingQuery.isPending ? ( ) : leagueGroups.length > 0 ? ( {finishedMatchesTitle} {finishedMatchesSubtitle} {showFinishedMatches ? ( finishedQuery.isPending ? ( ) : finishedLeagueGroups.length > 0 ? ( ) : ( {noFinishedMatchesLabel} ) ) : null} ) : ( {t("no-upcoming-matches")} )} {t("my-coupon")} {selectedMatchIds.length > 0 ? t("manual-selection-active") : t("automatic-selection-active")} {items.length > 0 ? ( ) : null} {/* Engine Mode Toggle */} {t("engine-mode-label")} setEngineMode("ai")} > AI setEngineMode("frequency")} > Frekans {engineMode === "ai" ? t("ai-mode-active") : t("freq-mode-active")} {engineMode === "frequency" ? ( ) : ( <> {t("strategy")} {strategies.map((entry) => { const active = activeStrategy === entry.key; const palette = strategyPalette(entry.key); return ( setActiveStrategy(entry.key)} > {entry.label} {active ? : null} {entry.description} ); })} {/* Match Count Input */} {t("match-count-label")} {matchCount} setMatchCount(Number(e.target.value))} style={{ width: "100%", accentColor: "teal", cursor: "pointer", }} /> 2 {t("match-count-auto", { count: allMatches.length })} 15 {t("selected-matches-panel-title")} {selectedMatchIds.length} {selectedMatches.length > 0 ? ( {selectedMatches.map((match: MatchResponseDto) => ( {match.matchName} {formatDate(match.mstUtc, locale)} ))} ) : ( {t("selected-matches-empty")} )} {selectedMatchIds.length > 0 ? t("manual-selection-helper") : t("automatic-selection-helper")} )} {t("suggested-bets-title")} {suggestedCoupon ? ( { strategies.find( (e) => e.key === suggestedCoupon.strategy, )?.label } ) : null} {suggestedCoupon ? ( {t("expected-win-rate")} {formatPercent( suggestedCoupon.expected_win_rate * 100, 0, )} {t("bet-count")} {suggestedCoupon.match_count} {suggestedBets.map((bet: SuggestedCouponBetDto) => ( {bet.match_name} {marketLabels?.[bet.market] || bet.market} •{" "} {bet.pick} {formatOdds(bet.odds)} {t("confidence-label")} {formatPercent(bet.confidence, 0)} {t("probability-label")} {formatPercent(bet.probability * 100, 0)} {t("risk-label")}: {bet.risk_level} {t("data-quality-label")}: {bet.data_quality} ))} {suggestedCoupon.rejected_matches?.length ? ( {t("rejected-matches-title")} {suggestedCoupon.rejected_matches.map((entry) => ( {entry.reason} ))} ) : null} ) : suggestErrorMessage ? ( {suggestErrorMessage} ) : ( {t("no-suggestion-yet")} )} {t("coupon")} {items.length > 0 ? ( {items.map((item: CouponItemDto) => ( {item.matchName || item.matchId} {getStoreItemLabel(item)} {formatOdds(item.odd)} ))} ) : ( {t("empty-coupon")} )} ); }