1

Location in the system

Super Agent is the highest level in the distribution chain. Every agent is created and managed by a super agent.

Regional management model:Super Agents are like a Regional Manager — manage many subordinate Agents, Each agent manages many Shops, each Shop manages many Players. The Super Agent's authority and vision spans the entire hierarchical tree.
Hierarchical tree — SuperAgent → Agent → Shop → Player
👑 Super Agent (Super Agent) SA Tier 0 — top of hierarchy
🧑‍💼 Agent A (Agent) AGT Created by SA · own cost rate
🏪 Shop A1 SHOP Belongs to Agent A
👤 Player 1, Player 2, ... PLR End users
🏪 Shop A2 SHOP Can move to another Agent
🧑‍💼 Agent B (Agent) AGT Independent cost rate
🏪 Shop B1 SHOP
🎯 What can SA do?
  • Create / Suspend / Terminate agent
  • Move shop between agents
  • Move player between shops
  • Allocate credits to agent
  • Configure game provider per-agent
  • View entire sub-hierarchy
📊 What does the agent see?
  • Own shops only
  • Players in own shops
  • Cannot create another agent
  • Cannot move shop to another SA
  • Cannot see SA credit balance
🔐 Auth endpoint

Login via

POST /super-agent-auth/login { email, password, captchaToken? }

Private Endpoint — separate from/agent-auth

2

Compare with Agent Portal

Superagent Portal inherits all the features of Agent Portal and adds multi-level management capabilities.

Feature Agent Portal Superagent Portal Notes
Dashboard & Analytics Same DashboardPage, but SA sees full-hierarchy summary
Manage Shops (own shops) (all shops) SA sees shops of EVERY sub-agent
Manage Players (own players) (all players) SA sees players in every shop
Create new Agent +SA createManagedAgent()
Suspend / Terminate Agent +SA 3 states: active → suspended → terminated
Move Shop to another Agent +SA moveManagedShop(shopId, targetAgentId)
Move Player to another Shop +SA moveManagedPlayer(playerId, targetShopId)
Allocate Credits to Agent +SA SuperAgentAgentCreditTrigger
Allocate Credits to Shop SellCreditsCashToAgentTrigger (shared)
Configure Game Provider per-Agent +SA putManagedAgentGameMappings()
Edit Agent Cost Rate +SA updateManagedAgentRate()
Xem Hierarchy Tree ~ (1 tier: shops) (2 tiers: agents+shops) SA: HierarchyMiniTree · Agent: AgentHierarchyMiniTree
Transactions History AgentTransactionsPage shared
Credit Purchase PurchaseCheckoutPage removed — purchase via shared CreditPurchaseModal
Broadcast / Popup SendBroadcastPage, SendShopPopupPage shared
PWA Service Worker +SA Workbox, StaleWhileRevalidate, precache
Summary:Superagent Portal = Agent Portal + ability to create and manage Agents + 2-level hierarchy + credit flow to agent + game mapping per-agent + PWA. Two completely new pages compared to Agent Portal areSuperAgentManagementPageandManagedAgentDetailPage.
3

Dealer Management

SuperAgentManagementPage.tsx— central page with 3 tabs:agents | shops | players. Route /agents, /shops, /playersUse the same component as propsection.

CREATE Create new Agent —createManagedAgent()

Form modal collects complete information and submitsPOST /super-agent/agents.

createManagedAgent({ name: string, // Agent name email: string, // Login email phone?: string, // Phone (react-phone-number-input) password: string, // Password (PASSWORD_REGEX) cost_rate: number, // 0–1 (toRate(costPercent)) can_create_shop_popup?: boolean, can_manage_shop_popup_permission?: boolean, default_shop_can_create_player_popup?: boolean })
Validation — Password
  • Minimum 8 characters (PASSWORD_MIN = 8)
  • Must contain at least 1 uppercase letter
  • Must contain at least 1 lowercase letter
  • Must contain at least 1 digit
const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/
Validation — Cost Rate
  • Input value: 0 – 100 (percent)
  • Convert: toRate(percent) → 0–1
  • When editing: must be greater than SA's own cost rate
// Input "5" → stored as 0.0500 const rate = Math.min(1, (n / 100).toFixed(4))
Popup permissions (3 checkboxes):
  • can_create_shop_popup — Agent can send popup/broadcast to shops/players
  • can_manage_shop_popup_permission — Agent can toggle popup permission per shop
  • default_shop_can_create_player_popup — Default: new shops created by this agent can send popups to players
Agent lifecycle — Suspend / Unsuspend / Terminate
🟢
Active
Operating normally
🟡
Suspended
Suspended
🟢
Active
Unsuspend
🔴
Terminated
Irreversible
// Suspend / Unsuspend (toggle) item.status === 'suspended' ? unsuspendManagedAgent(item.id) : suspendManagedAgent(item.id) // Terminate — requires confirmation of typing "terminate" const confirmWord = window.prompt(`Type "terminate" to terminate ${item.email}`) if ((confirmWord ?? '').trim().toLowerCase() !== 'terminate') return
MOVE Move Shop to another Agent

Trong tab shops, each row has a "Move" button. SA selects target agent (target agent ≠ current agent), confirm →moveManagedShop(shopId, targetAgentId).

PUT /super-agent/shops/{shopId}/move { target_agent_id: string }
UI Guard:The shop's current agent is disabled in the dropdown — cannot "move" to that agent itself.
MOVE Move Player to another Shop

Trong tab players, each row has a "Move" button. SA selects the target shop (target shop ≠ current shop), confirm →moveManagedPlayer(playerId, targetShopId).

PUT /super-agent/players/{playerId}/move { target_shop_id: string }
Context:Dropdown displaysall shops(with the shop owner's agent name) so SA has enough context when choosing.
Search & Filter in each tab
Tab Agents
  • Search by name / email / phone
  • useMemo: filteredAgents
Tab Shops
  • Search by name / email / phone
  • Filter by agent (dropdown)
  • useMemo: filteredShops
Tab Players
  • Search by username / email / phone
  • Filter by shop (dropdown)
  • useMemo: filteredPlayers
4

Agent Details — ManagedAgentDetailPage

Route /agents/:agentId. The page has 4 tabs:Info · Games · Shops · Players. Tabs are managed via URL search param?tab=....

Tab Info
  • Name, email, status badge (active/suspended/terminated)
  • Credit balance of agent (agent.credit_balance)
  • Button Suspend / Unsuspend / Terminate
  • SuperAgentAgentCreditTrigger — distribute credits
  • Edit Cost Rate inline (edit → enter → save)
  • Guard: new cost rate must exceed SA's own rate
Tab Games
  • Fetch fetchManagedAgentGameMappings(agentId)
  • Fetch fetchPublicGameProviders()
  • parentGamePool: SA's current game set (filter)
  • 2 modes: All (inherit SA pool) or Selected
  • Checkbox list — select specific game providers
  • Save → putManagedAgentGameMappings(agentId, ids[])
  • Empty array = "all" (inherit SA pool)
Tab Shops
  • Stat: Total shops / Active shops
  • Table: shop name, email, status
  • Filter: show only this agent's shops (s.agent?.id === agentId)
Tab Players
  • Stat: Total players / Active players
  • Table: username/email/phone, shop, status
  • Filter: p.agent_id === agentId
URL-driven tab navigation
// Tab state in URL — not lost when F5 const [searchParams, setSearchParams] = useSearchParams() const tabParam = searchParams.get('tab') // Tabs: 'info' | 'games' | 'shops' | 'players' // /agents/123 → info tab (default) // /agents/123?tab=games → games tab // /agents/123?tab=shops → shops tab // /agents/123?tab=players → players tab
5

Configure Game Provider per-Agent

The SA can limit or expand the game providers that each agent has access to. Inherited and overridden logic from SA pool to agent pool.

Stream inherits Game Provider
👑
SA Game Pool
Source set
parentGamePool[]
🔧
SA Configuration
Select: All or subset
for each Agent
🧑‍💼
Agent Pool
games_ids[] or
inherit entire SA pool
🏪
Shop Players
Can play games
in agent pool
// Get the agent's current game mappings const [mapRes, gpRes] = await Promise.all([ fetchManagedAgentGameMappings(agentId), fetchPublicGameProviders() ]) // mapRes.data.game_ids → [] = "all" | [...ids] = subset // mapRes.data.super_agent_game_ids → SA's own pool (parent) // Save (PUT) const payload = gameMode === 'all' ? [] : selectedGameIds putManagedAgentGameMappings(agentId, payload)
Filter rules:The dropdown checkbox only shows the games in itparentGamePoolof SA. If SA has no games in the pool (parentGamePool.length === 0), displays all game providers that are not in maintenance state.
GET /super-agent/agents/{id}/game-mappings
Get the agent's current game mappings, including super_agent_game_ids
PUT /super-agent/agents/{id}/game-mappings
Update game ids for agents —[]= inherit SA pool
GET /public/game-providers
List of public game providers (filter status !== 'maintenance')
6

Credits Distribution

SA distributes credits to agents — a completely new mechanism compared to Agent Portal (which only distributes credits to shops).

SuperAgentAgentCreditTrigger — Specialized component

Component SuperAgentAgentCreditTrigger.tsx in components/credits/. Appears inManagedAgentDetailPageInfo tab, next to the govern agent buttons.

<SuperAgentAgentCreditTrigger agentId={agent.id} agentName={agent.name} agentCostRate={agent.costRate} // decimal 0–1 onSuccess={() => load()} size="sm" className="shadow-sm" />
Modal flow
  • Click "Credit" button → open modal
  • Load SA's own balance (fetchDashboard())
  • Show agent name + current cost rate
  • Enter amount (credits)
  • Preview: credits → cash equivalent
  • Submit → distributeCreditsToManagedAgent()
  • Close → callback onSuccess()
Cash preview logic
// When entering 100 credits // agent cost rate = 0.95 (95%) credits = 100 cash = credits × effectiveCostRate = 100 × 0.95 = $95.00 // Display: "Credits to agent: 100.00" "Cash recorded: $95.00 USD"
Payment mechanism similar to Agent → Shop: When the SA issues credits to the Agent, the system records a payment record — the amount of cash that the Agent must pay to the SA. Recipe:cash = credits × agent_cost_rate. Same rules as Agents granting credits to Shops.
Credits flow throughout the system
🏦
Platform
Origin
👑
Super Agent
Buy credits
🧑‍💼
Agent
SuperAgentAgentCreditTrigger
🏪
Shop
SellCreditsCashToAgentTrigger
👤
Player
Deposit / withdraw
POST /super-agent/agents/{id}/credits
Allocate credits from SA wallet to Agent wallet
7

Multilevel Visibility — HierarchyMiniTree

Component HierarchyMiniTree.tsx in components/dashboard/. Display 2 levels: SA → Agents → Shops. Unlike Agent Portal, there is only 1 level (Agent → Shops).

HierarchyMiniTree feature
  • Expandable tree — root SA node expand/collapse
  • Each Agent can expand/collapse independently to view shops
  • StatusDot per-node (green=active, amber=suspended, gray=terminated)
  • Shops displayed as card grid (1-3 cols by viewport)
  • Each shop card links to /agents/{id}
  • Date range filter (from/to) — period statistics
  • Summary row: total deposits, withdrawals, new users, total users
  • Stats per-shop: deposits, withdrawals, new users, total users
  • API: fetchSuperAgentShopHierarchyStats({from, to})
  • Default range: last 30 days
// Props type Props = { superAgentName: string; // SA root node name agents: SuperManagedAgent[]; // From fetchManagedAgents() shops: SuperManagedShop[]; // From fetchManagedShops() } // Internal grouping const shopsByAgent = useMemo(() => { // Map<agentId, SuperManagedShop[]> // sorted alphabetically per agent }, [shops])
2 levels vs 1 level: Agent Portal usedAgentHierarchyMiniTree— only see Agent → Shops (1 level). Superagent Portal usedHierarchyMiniTree— see SA → Agents → Shops (2 levels). SA also sees statistics summarizing the entire hierarchy in the summary row.
Data Loading — Promise.all
// SuperAgentManagementPage and DashboardPage are both used const [agentsRes, shopsRes, playersRes] = await Promise.all([ fetchManagedAgents(), // GET /super-agent/agents fetchManagedShops(), // GET /super-agent/shops fetchManagedPlayers() // GET /super-agent/players ]) // Parallel fetch — all fires at once
8

PWA Capability — Service Worker

Superagent Portal is the only repo in the ecosystem that has a Service Worker — geared towards mobile usability and working when the network is unstable.

File: src/service-worker.js + src/serviceWorkerRegistration.ts — using Workbox (Google). Automatically injected by CRA + Workbox webpack plugin at build time.
📦
Precache & Route

precacheAndRoute(self.__WB_MANIFEST)— cache all static assets (JS, CSS, fonts) at install time. Automatically updated when new builds.

📄
SPA Navigation

createHandlerBoundToURL('/index.html')— intercept navigate requests, returnedindex.html for React Router handle client-side.

🖼️
Image Cache

StaleWhileRevalidate strategy for .pngfiles. Cache name "images", max 100 entries. Display cached images immediately, revalidate implicitly.

🔄
Skip Waiting

clientsClaim()— The new SA is activated immediately after installation, no need to wait for the tab to close/open. Guaranteed quick updates.

📱
Mobile Intent

SA portal has a Service Worker suggested for use on mobile (tablet/phone) — suitable for the role of mobile "regional manager", needing access anytime, anywhere.

⚙️
Workbox Strategies

Useworkbox-core, workbox-routing, workbox-strategies, workbox-expiration, workbox-precaching.

9

Superagent Portal Routes

React Router v6 structure — all routes are wrappedProtectedRoute(apart from/login).

Path Component SA exclusive? Description
/dashboard DashboardPage Dashboard with HierarchyMiniTree (2 tiers)
/agents SuperAgentManagementPage (section="agents") SA Only Agent list + create new + govern
/agents/:agentId ManagedAgentDetailPage SA Only Agent detail: Info/Games/Shops/Players
/shops SuperAgentManagementPage (section="shops") All shops + filter by agent + move
/players SuperAgentManagementPage (section="players") All players + filter by shop + move
/transactions AgentTransactionsPage Transaction history (shared)
/purchase/checkout PurchaseCheckoutPage removed — purchase via CreditPurchaseModal Purchase credits (shared)
/payment/return AgentPaymentReturnPage Payment result (shared)
/login LoginPage Call POST /super-agent-auth/login
Note: Routes /shopsand/playersuse togetherSuperAgentManagementPagewith propsectiondifferent. Default route/redirect to/dashboard. Route *(wildcard) also redirects to/dashboard.
10

Technical Stack & Architecture

Main stack
React 18 · CRA + Craco TypeScript 4.9 React Router v6 react-hook-form v7 Axios Tailwind CSS 3 Lucide React react-hot-toast react-phone-number-input v3 Workbox (PWA)
Internal packages
@kioskgaming/ui @kioskgaming/page-loading
  • @kioskgaming/page-loadingPageLoadingIndicator variant="page" and variant="inline"
  • @kioskgaming/ui — shared UI components
Directory structure
kioskgaming_superagent/src/ ├── pages/ │ ├── SuperAgentManagementPage.tsx ← MAJOR: 3-tab management │ ├── ManagedAgentDetailPage.tsx ← MAJOR: agent detail 4-tab │ ├── DashboardPage.tsx │ ├── ShopsPage.tsx │ ├── ShopDetailPage.tsx │ ├── AgentTransactionsPage.tsx │ ├── AgentPaymentReturnPage.tsx │ ├── PurchaseCheckoutPage abandoned — purchase via CreditPurchaseModal.tsx │ ├── SendBroadcastPage.tsx │ ├── SendShopPopupPage.tsx │ └── SettingsPage.tsx ├── components/ │ ├── credits/ │ │ ├── SuperAgentAgentCreditTrigger.tsx ← SA exclusive │ │ ├── SellCreditsCashToAgentTrigger.tsx │ │ ├── AvailableCreditsCallout.tsx │ │ └── CreditPurchaseTrigger.tsx │ ├── dashboard/ │ │ ├── HierarchyMiniTree.tsx ← SA 2-level tree │ │ └── AgentHomeRecentTransactions.tsx │ ├── layout/, auth/, shops/, ui/ ├── services/ │ └── api.ts ← /super-agent/* endpoints ├── types/ │ └── index.ts (SuperManagedAgent, etc.) ├── service-worker.js ← Workbox PWA └── serviceWorkerRegistration.ts
API Client — Axios with JWT interceptor
// services/api.ts — createClient() client.interceptors.request.use((config) => { const token = localStorage.getItem(STORAGE_KEYS.AUTH_TOKEN) if (token) config.headers.Authorization = `Bearer ${token}` return config }) client.interceptors.response.use( (res) => res, (error) => { if (error.response?.status === 401) window.dispatchEvent(new Event('auth:unauthorized')) return Promise.reject(error) } )
Timeout 15s: Axios config timeout: 15000. Unauthorized 401 generates event'auth:unauthorized'— AuthProvider listens and automatically logs out.
Types specific to SA
// Super-Agent-specific types in types/index.ts type SuperManagedAgent = { id: string; name: string; email: string; phone?: string; status: 'active' | 'suspended' | 'terminated'; costRate?: number; // 0–1 credit_balance?: number; } type SuperManagedShop = { id: string; name: string; agent?: { id: string; name: string }; costRate?: number; status: string; } type SuperManagedPlayer = { id: string; username?: string; shop_id: string; shop_name?: string; agent_id: string; agent_name?: string; }
11

Aggregated Endpoints API

All endpoints under prefix/super-agent/and/super-agent-auth/.

Method Endpoint Function Purpose
POST /super-agent-auth/login loginWithEmailPassword() SA login
GET /super-agent/agents fetchManagedAgents() Sub-agent list
POST /super-agent/agents createManagedAgent() Create new agent
PUT /super-agent/agents/{id}/suspend suspendManagedAgent() Suspend agent
PUT /super-agent/agents/{id}/unsuspend unsuspendManagedAgent() Unsuspend agent
POST /super-agent/agents/{id}/terminate terminateManagedAgent() Terminate agent (irreversible)
PUT /super-agent/agents/{id}/rate updateManagedAgentRate() Edit agent cost rate
POST /super-agent/agents/{id}/credits distributeCreditsToManagedAgent() Allocate credits to agent
GET /super-agent/agents/{id}/game-mappings fetchManagedAgentGameMappings() Agent game mappings
PUT /super-agent/agents/{id}/game-mappings putManagedAgentGameMappings() Update game access
GET /super-agent/shops fetchManagedShops() All shops
PUT /super-agent/shops/{id}/move moveManagedShop() Move shop to another agent
GET /super-agent/players fetchManagedPlayers() All players
PUT /super-agent/players/{id}/move moveManagedPlayer() Move player to another shop
GET /super-agent/shop-hierarchy-stats fetchSuperAgentShopHierarchyStats() Stats hierarchy (deposits, withdrawals, users)