Kỹ thuật chi tiết — Super Agent Portal
Đi sâu vào cơ chế thực thi của cổng Siêu Đại Lý: bản chất là một fork của Agent Portal (CRA React 18), bổ sung tầng quản trị Agent, phân phối credits xuống agent, game mapping per-agent, cây phân cấp 2 cấp và Service Worker (PWA). Đọc sau báo cáo tổng quan 05-superagent.html.
Stack & tư duy tổng thể
Trước khi đọc code, nắm 6 đặc điểm "lạ" nhất của app này. Phần lớn xuất phát từ một sự thật: repo này là bản sao của Agent Portal, được mở rộng thêm chứ không viết lại từ đầu — rất nhiều tên gọi trong code vẫn là "Agent".
App là 1 fork của Agent Portal
package.json vẫn ghi "name": "kioskgaming-agent", README.md ghi "Agent Portal", và toàn bộ type/hook dùng tiền tố Agent* (AgentProfile, useAgentTransactions…). Super Agent = Agent + tầng quản trị agent.
Dùng axios, không phải fetch
Khác hẳn Kiosk Frontend. Một singleton client trong services/api.ts với interceptor gắn Bearer token và bắt 401. timeout: 15000.
Routing thật bằng React Router v6
Không phải state machine như Kiosk. Có BrowserRouter, <Routes>, deep-link, ProtectedRoute bọc mọi route trừ /login.
401 = logout, KHÔNG refresh
Login trả cả refreshToken nhưng app không lưu cũng không dùng. Gặp 401 → bắn event auth:unauthorized → AuthProvider xoá localStorage. Hết hạn = đăng nhập lại.
PWA: repo duy nhất có Service Worker
service-worker.js + Workbox. Chỉ đăng ký ở production (NODE_ENV==='production'). Hỗ trợ offline, precache, image cache.
Tailwind v3 + theme cam
Có tailwind.config.js (HSL CSS vars). UI thực tế dùng tông cam (orange-500/600), không phải vàng — màu vàng chỉ là theme của tài liệu này.
Người mới nên mở: src/App.tsx (routes) → src/services/api.ts (toàn bộ endpoint) → src/hooks/useAuth.tsx (auth) → src/pages/SuperAgentManagementPage.tsx và ManagedAgentDetailPage.tsx (2 trang độc quyền SA). 5 file này là ~75% phần "khác biệt so với Agent".
Cấu trúc thư mục src/
Cây CRA chuẩn: pages/ cho route component, components/ cho UI con, services/api.ts tập trung mọi HTTP. Các file ★ là phần độc quyền của Super Agent.
Đừng nhầm: AgentProfile, AgentDashboardData, AgentTransactionsPage, fetchAgentTransactions… đều phục vụ Super Agent. Type SuperManagedAgent/Shop/Player mới là "agent/shop/player mà SA quản lý".
Màn hình khởi động & đăng ký Service Worker
Điều gì xảy ra khi trình duyệt mở app. Khác Kiosk Frontend, đây là CRA SPA thuần client.
Mount React 18 createRoot; rồi gọi serviceWorkerRegistration.register() ở cuối file.
<AuthProvider> → <BrowserRouter> → <Routes> + <Toaster/> (react-hot-toast).
Đọc agent_auth_token & agent_user_data từ localStorage; nếu hợp lệ → isAuthenticated=true. Trong khi đọc, isLoading=true.
/ redirect /dashboard. Chưa đăng nhập → ProtectedRoute đẩy về /login (giữ state.from để quay lại).
const root = ReactDOM.createRoot(document.getElementById('root')!); root.render(<React.StrictMode><App /></React.StrictMode>); // SW chỉ thực sự register khi NODE_ENV === 'production' serviceWorkerRegistration.register();
Kiosk là màn hình chạm 1 phiên, không cần URL. Super Agent là công cụ quản trị nhiều trang, cần deep-link, back/forward, bookmark — nên dùng router thật. Mỗi vai trò chọn mô hình phù hợp.
Hệ thống phân cấp SuperAgent → Agent → Shop → Player
Đây là khái niệm trung tâm. Super Agent đứng đỉnh chuỗi B2B: mua credits từ platform, tạo & quản lý nhiều Agent, mỗi Agent quản nhiều Shop, mỗi Shop có nhiều Player.
SA làm được gì (qua /super-agent/*)
- Tạo / Suspend / Unsuspend / Terminate Agent
- Sửa
cost_ratecủa Agent - Cấp credits cho Agent (debit ví SA)
- Cấu hình game provider per-Agent
- Move Shop sang Agent khác, Move Player sang Shop khác
- Xem toàn bộ agents/shops/players + thống kê
"Đồng dạng" với cặp Agent→Shop
Quan hệ SA→Agent là bản sao quan hệ Agent→Shop một cấp lên trên: cùng công thức cash = credits × cost_rate, cùng kiểu vòng đời active/suspended/terminated, cùng mô hình "ví cha nạp xuống ví con".
| Cấp | Type trong code | Nguồn dữ liệu |
|---|---|---|
| Super Agent (mình) | AgentProfile | login + /super-agent/dashboard |
| Agent (con) | SuperManagedAgent | GET /super-agent/agents |
| Shop (cháu) | SuperManagedShop | GET /super-agent/shops (kèm agent) |
| Player (chắt) | SuperManagedPlayer | GET /super-agent/players (kèm shop_name, agent_name) |
API không trả cây lồng nhau. SA fetch 3 danh sách phẳng rồi tự nối ở client: shop.agent?.id nối shop với agent, player.shop_id/player.agent_id nối player với shop/agent. HierarchyMiniTree group lại bằng useMemo.
Routing & ProtectedRoute
React Router v6 trong App.tsx. Mọi route (trừ /login) bọc trong ProtectedRoute; route không khớp redirect về /dashboard.
| Path | Component | Ghi chú |
|---|---|---|
/login | LoginPage | Public; có Turnstile captcha (tuỳ env) |
/ | — | Navigate → /dashboard |
/dashboard | DashboardPage | Stat cards + HierarchyMiniTree |
/agents | SuperAgentManagementPage section="agents" | SA only danh sách + tạo agent |
/agents/:agentId | ManagedAgentDetailPage | SA only chi tiết, 4 tab |
/shops | SuperAgentManagementPage section="shops" | Tất cả shop + move |
/players | SuperAgentManagementPage section="players" | Tất cả player + move |
/agents/new | CreateManagedAgentPage | Tạo managed agent |
/online-transactions | OnlineBalanceTransactionsPage | Ledger USD + rút crypto |
/cash-transactions | CashTransactionsLedgerPage | Giao dịch tiền mặt |
/payment/return | AgentPaymentReturnPage | Callback mua credit online |
/transactions | AgentTransactionsPage | Lịch sử ví SA |
/payment/return | AgentPaymentReturnPage | Trang quay về sau cổng thanh toán |
* | — | Navigate → /dashboard |
const { isAuthenticated, isLoading } = useAuth(); if (isLoading) return <PageLoadingIndicator variant="section" />; if (!isAuthenticated) return <Navigate to="/login" replace state={{ from: location }} />; return <>{children}</>;
ProtectedRoute chỉ kiểm tra "đã đăng nhập chưa", không kiểm tra vai trò. Việc một route là "độc quyền SA" được đảm bảo bởi backend qua prefix /super-agent/* + token SA. Frontend không có guard theo role.
services/api.ts — trung tâm gọi API
Một file duy nhất chứa toàn bộ hàm gọi backend, export theo từng nghiệp vụ. Component import hàm cần dùng, tự quản loading/error.
- Base URL:
REACT_APP_API_BASE_URL || 'http://localhost:3001/api'(prefix/apinằm sẵn trong base) - Request: đọc
agent_auth_tokentừ localStorage → thêmAuthorization: Bearer <token> - Response 401:
window.dispatchEvent(new Event('auth:unauthorized'))rồi reject — KHÔNG refresh - Timeout:
15000ms - Envelope: response chuẩn
{ success, data?, message? }
const client = axios.create({ baseURL: API_BASE_URL, timeout: 15000 }); 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((r) => r, (error) => { if (error.response?.status === 401) window.dispatchEvent(new Event('auth:unauthorized')); return Promise.reject(error); });
/agent/* (không phải /super-agent/*)Vì là fork, các tính năng kế thừa vẫn gọi prefix cũ: fetchShops() → /agent/shops, fetchAgentPlayers() → /agent/players, broadcasts/in-app-popups → /agent/broadcasts, /agent/in-app-popups/*, low-balance → /agent/settings/low-balance-warning, cấp credits xuống shop → POST /agent/credits/distribute. Backend phải chấp nhận token SA cho cả 2 prefix. Khi debug 403/404, luôn kiểm tra prefix thực của hàm.
Luồng đăng nhập & JWT
Đăng nhập 1 bước email + password (kèm captcha tuỳ chọn). Token lưu localStorage. Không có mã OTP như Kiosk, không có refresh token.
loginWithEmailPassword(email, password, captchaToken?) → POST /super-agent-auth/login. Nếu REACT_APP_TURNSTILE_ENABLED bật thì bắt buộc có Cloudflare Turnstile token.
Lấy res.data.agent hoặc res.data.superAgent (backend trả khác key tuỳ phiên bản), chuẩn hoá qua toAgentProfile() (loại bỏ passwordHash).
login(profile, accessToken) ghi agent_auth_token + agent_user_data vào localStorage; bắn agent-auth-token-changed. refreshToken bị bỏ qua.
navigate(from) — quay lại trang đã chặn trước đó (qua location.state.from) hoặc /dashboard.
Lưu & khôi phục
localStorage: agent_auth_token, agent_user_data. Khi reload, AuthProvider đọc lại; JSON lỗi → tự logout().
Logout = thuần client
logout() chỉ xoá localStorage + reset state, không gọi API. Cũng được trigger tự động khi nhận event auth:unauthorized (401).
(1) Không có refresh token → access token hết hạn là user bị đá ra login ngay (mọi request đang chạy fail 401). (2) STORAGE_KEYS dùng chung tên với Agent Portal (agent_auth_token). Nếu chạy SA và Agent app trên cùng origin (cùng host:port) chúng sẽ ghi đè token của nhau. App tránh điều này bằng cách dùng port khác (SA mặc định 3003).
Quản lý state
Tối giản: chỉ 1 Context (Auth). Mọi dữ liệu nghiệp vụ là local state trong từng page, tải bằng Promise.all khi mount.
- AuthContext (
useAuth): user, token, isAuthenticated, isLoading - Local state mỗi page: agents/shops/players, loading, modal, search/filter
- react-hook-form cho form login
- react-hot-toast cho thông báo (thay cho notificationService)
- useMemo cho filter/group (
filteredAgents,shopsByAgent…)
- Redux / Zustand / Jotai
- React Query / SWR (mọi fetch tự viết tay)
- Cache cross-page — mỗi page tự fetch lại
- Optimistic update — sau mutate luôn gọi lại
load()
const [agentsRes, shopsRes, playersRes] = await Promise.all([ fetchManagedAgents(), // GET /super-agent/agents fetchManagedShops(), // GET /super-agent/shops fetchManagedPlayers() // GET /super-agent/players ]); setAgents(agentsRes.data ?? []); /* …setShops, setPlayers */
SuperAgentManagementPage — 3 section trong 1 component
Một component dùng cho 3 route (/agents, /shops, /players) qua prop section. Lưu ý: đây là 3 route riêng, không phải tab — chuyển section = đổi URL, component re-mount & fetch lại cả 3 danh sách.
| section | Hành động chính | Search / Filter |
|---|---|---|
agents | Tạo agent (createManagedAgent), Suspend/Unsuspend, Terminate, link vào chi tiết | Search name/email/phone → filteredAgents |
shops | Move shop sang agent khác | Search + filter theo agent → filteredShops |
players | Move player sang shop khác | Search + filter theo shop → filteredPlayers |
Tạo Agent — validation phía client
const PASSWORD_MIN = 8; const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; // % nhập vào (0–100) → rate thập phân (0–1), làm tròn 4 chữ số const toRate = (percent: string) => Math.min(1, Number((Number(percent) / 100).toFixed(4))); createManagedAgent({ name, email, phone, password, cost_rate: toRate(form.costPercent), can_create_shop_popup, can_manage_shop_popup_permission, default_shop_can_create_player_popup });
Phone & popup permissions
Input điện thoại dùng react-phone-number-input (mặc định US). 3 checkbox uỷ quyền popup/broadcast cho agent: can_create_shop_popup, can_manage_shop_popup_permission, default_shop_can_create_player_popup.
Terminate cần gõ xác nhận
window.prompt('Type "terminate"…'); phải gõ đúng chữ terminate mới gọi API. Đây là cơ chế chống nhầm, không phải modal đẹp.
Move Shop / Move Player
PATCH /super-agent/shops/{id}/move body { target_agent_id }. Dropdown disable agent hiện tại của shop để không "move vào chính nó".
PATCH /super-agent/players/{id}/move body { target_shop_id }. Dropdown hiện tất cả shop kèm tên agent chủ để có ngữ cảnh; disable shop hiện tại.
Tài liệu tổng quan ghi PUT/POST cho suspend/terminate/move, nhưng code dùng PATCH cho suspend/unsuspend/terminate/rates/move. Riêng tạo agent & cấp credits dùng POST; game-mappings dùng PUT. Luôn lấy verb từ api.ts.
ManagedAgentDetailPage — 4 tab điều khiển bằng URL
Route /agents/:agentId. Tab lưu trong query ?tab= (giữ được khi F5). Khác hẳn management page (dùng route), đây là tab thật trong cùng URL.
const [searchParams, setSearchParams] = useSearchParams(); const activeTab = isAgentDetailTab(searchParams.get('tab')) ? searchParams.get('tab') : 'info'; // info là default → xoá param 'tab' khỏi URL cho gọn const setActiveTab = (t) => setSearchParams((p) => { const n = new URLSearchParams(p); t === 'info' ? n.delete('tab') : n.set('tab', t); return n; }, { replace: true });
Tab Info
Status badge, credit_balance, nút Credit (SuperAgentAgentCreditTrigger), Suspend/Terminate, và sửa cost rate inline. Gợi ý "phải cao hơn rate của SA" hiển thị từ superAgentProfile.costRate.
Tab Games
Tải song song fetchManagedAgentGameMappings + fetchPublicGameProviders. Chọn All hoặc Selected. Xem mục 10.
Tab Shops
Lọc client: shops.filter(s => s.agent?.id === agentId). Hiện total/active + bảng shop.
Tab Players
Lọc client: players.filter(p => p.agent_id === agentId). Hiện total/active + bảng player.
load() gọi fetchManagedAgents/Shops/Players (toàn bộ) rồi .find()/.filter() theo agentId ở client. Với hệ thống lớn đây là điểm tốn băng thông; nếu cần tối ưu, đề xuất backend thêm GET /super-agent/agents/:id.
Cấu hình Game Provider theo từng Agent
SA giới hạn tập game mỗi agent được phép dùng. Logic kế thừa: agent chỉ thấy game nằm trong "pool" của SA; lưu mảng rỗng = "kế thừa toàn bộ".
Promise.all([fetchManagedAgentGameMappings(agentId), fetchPublicGameProviders()]). Mapping trả game_ids (của agent) + super_agent_game_ids (pool SA).
visibleGameProviders: nếu parentGamePool.length > 0 chỉ hiện game thuộc pool đó; nếu pool rỗng → hiện mọi provider status !== 'maintenance'.
game_ids rỗng → mode All (kế thừa pool SA); có phần tử → mode Selected với checkbox.
putManagedAgentGameMappings(agentId, payload) với payload = mode==='all' ? [] : selectedGameIds.
| Method | Endpoint | Mục đích |
|---|---|---|
| GET | /super-agent/agents/{id}/game-mappings | Trả { game_ids, super_agent_game_ids } |
| PUT | /super-agent/agents/{id}/game-mappings | Body { game_ids } — [] = kế thừa SA |
| GET | /game-providers | Danh mục public, response { data: { items[] } } |
/game-providersKhông phải /public/game-providers như tài liệu tổng quan. Dữ liệu nằm trong data.items[] (mỗi item: { id, name, code, status? }), được lọc bỏ status === 'maintenance' trước khi render.
Phân phối Credits
Hai dòng tiền: (1) SA mua credits từ platform; (2) SA cấp credits xuống Agent. Cấp xuống Shop dùng lại component của Agent Portal.
SA → Agent: SuperAgentAgentCreditTrigger
Component đặt ở tab Info của chi tiết agent. Mở modal → tải số dư SA (fetchDashboard) → nhập amount → preview cash → submit.
// Preview: cash mà agent phải trả cho SA const effectiveCostRate = Math.min(1, Math.max(0, Number(agentCostRate ?? 0))); const cashToCollect = credits * effectiveCostRate; // vd: 100 credits × 0.95 = $95.00 (agent trả SA) await distributeCreditsToManagedAgent(agentId, amount); // POST /super-agent/agents/{id}/cash-credits — bán credit cho agent (cash)
SA mua credits từ platform
purchaseCredits(amount, paymentMethod?) → POST /super-agent/credits/purchase. /createManagedAgentCashCredits — POST /super-agent/agents/:id/cash-credits hỗ trợ phương thức 0x/crypto (LinkMePay), trả paymentUrl để redirect.
Cấp credits xuống Shop (dùng chung)
distributeCredits(shopId, amount) → POST /agent/credits/distribute body { shop_id, amount } — đây là endpoint của Agent, không phải super-agent.
/credits/distributecreateManagedAgentCashCredits gọi POST /super-agent/agents/{id}/cash-credits. WithdrawCreditsFromAgentTrigger thu hồi credit. Công thức: cash = credits × agent_cost_rate.
HierarchyMiniTree — cây 2 cấp + thống kê
Khác Agent Portal (1 cấp Agent→Shop), SA thấy 2 cấp: SA → Agents → Shops, kèm thống kê deposit/withdrawal/users theo khoảng ngày.
type Props = { superAgentName: string; agents: SuperManagedAgent[]; shops: SuperManagedShop[] }; // Group shop theo agent (Map<agentId, shops[]>), sort theo tên const shopsByAgent = useMemo(() => { /* ... */ }, [shops]); // Thống kê theo kỳ (mặc định 1 tháng gần nhất) fetchSuperAgentShopHierarchyStats({ from, to }); // GET /super-agent/shops/stats?from=&to=
Tương tác
Root SA expand/collapse; mỗi agent expand riêng để xem shop dạng card grid; StatusDot mỗi node (active/suspended/terminated); shop card link tới /agents/{agentId}.
Thống kê
Date range from/to (mặc định 30 ngày). Summary row tổng deposit/withdrawal/new users/total users; mỗi shop có stats riêng từ stats.shops[] map theo id.
/super-agent/shops/statsKhông phải /super-agent/shop-hierarchy-stats. Response: { range, summary, shops[] }; mỗi shop có agent_id, agent_name, total_users, new_users, total_deposit, total_withdrawal.
Service Worker (Workbox)
Repo duy nhất trong hệ sinh thái có SW — phù hợp vai trò "regional manager" cần dùng trên tablet/mobile. Chỉ chạy ở production.
clientsClaim(); // SW mới claim ngay, không chờ precacheAndRoute(self.__WB_MANIFEST); // precache asset build-time // SPA: navigate request → trả index.html cho React Router registerRoute(isNavigate, createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')); // Ảnh .png cùng origin → StaleWhileRevalidate, tối đa 100 entry registerRoute(isPng, new StaleWhileRevalidate({ cacheName: 'images', plugins: [new ExpirationPlugin({ maxEntries: 100 })] }));
Precache
__WB_MANIFEST được Workbox inject lúc build; cache JS/CSS/font tĩnh.
SPA fallback
Navigate (không phải file, không bắt đầu /_) → index.html.
SKIP_WAITING
Lắng nghe message SKIP_WAITING → self.skipWaiting() để cập nhật nhanh.
serviceWorkerRegistration.register() chỉ chạy khi process.env.NODE_ENV === 'production'. Chạy npm start (dev) sẽ không đăng ký SW — muốn test PWA phải npm run build rồi serve thư mục build/.
Config & biến môi trường
Tập trung ở src/config/env.ts + src/constants/index.ts. Mọi biến phải có tiền tố REACT_APP_ (quy ước CRA) mới dùng được ở client.
| Biến | Mặc định | Ý nghĩa |
|---|---|---|
REACT_APP_API_BASE_URL | http://localhost:3001/api | Base URL backend (đã gồm /api) — chung backend với admin |
REACT_APP_APP_NAME | Kiosk Gaming — Agent | Tên app (di sản fork, vẫn ghi "Agent") |
REACT_APP_TURNSTILE_ENABLED | false | Bật Cloudflare Turnstile ở màn login |
REACT_APP_TURNSTILE_SITE_KEY | — | Site key Turnstile |
PORT | 3003 | Tránh đụng admin (3000) — quan trọng để không clash localStorage token |
.env.exampleCopy .env.example → .env. Backend cần set AGENT_FRONTEND_URL trỏ về app này (vd http://localhost:3003) để redirect thanh toán quay về đúng /payment/return. CORS: thêm hostname vào bảng domains (type admin) hoặc DISABLE_CORS=true khi dev.
API Endpoints tổng hợp (theo code thật)
Trích từ services/api.ts. Chú ý prefix kép: /super-agent/* (tính năng SA) xen lẫn /agent/* (kế thừa từ fork).
| Method | Endpoint | Hàm | Mục đích |
|---|---|---|---|
| POST | /super-agent-auth/login | loginWithEmailPassword | Đăng nhập SA |
| GET | /super-agent/dashboard | fetchDashboard | Số dư + đếm shop/agent |
| GET | /super-agent/agents | fetchManagedAgents | Danh sách agent |
| POST | /super-agent/agents | createManagedAgent | Tạo agent |
| PATCH | /super-agent/agents/{id}/suspend | suspendManagedAgent | Đình chỉ |
| PATCH | /super-agent/agents/{id}/unsuspend | unsuspendManagedAgent | Bỏ đình chỉ |
| PATCH | /super-agent/agents/{id}/terminate | terminateManagedAgent | Chấm dứt ({confirm:true}) |
| PATCH | /super-agent/agents/{id}/rates | updateManagedAgentRate | Sửa cost_rate (số nhiều: rates) |
| POST | /super-agent/agents/{id}/cash-credits | createManagedAgentCashCredits | Bán credit SA→Agent (cash) |
| POST | /super-agent/agents/{id}/credits/withdraw | withdrawCreditsFromManagedAgent | Thu hồi credit |
| GET | /super-agent/agents/{id}/game-mappings | fetchManagedAgentGameMappings | Lấy game của agent |
| PUT | /super-agent/agents/{id}/game-mappings | putManagedAgentGameMappings | Lưu game ({game_ids}) |
| GET | /super-agent/shops | fetchManagedShops | Tất cả shop |
| PATCH | /super-agent/shops/{id}/move | moveManagedShop | Move shop ({target_agent_id}) |
| GET | /super-agent/shops/stats | fetchSuperAgentShopHierarchyStats | Thống kê hierarchy |
| GET | /super-agent/players | fetchManagedPlayers | Tất cả player |
| PATCH | /super-agent/players/{id}/move | moveManagedPlayer | Move player ({target_shop_id}) |
| POST | /super-agent/credits/purchase | purchaseCredits | Mua credits từ platform |
| GET | /super-agent/transactions | fetchAgentTransactions | Lịch sử ví SA |
| GET | /game-providers | fetchPublicGameProviders | Danh mục game public |
| POST | /agent/credits/distribute /agent/* | distributeCredits | Cấp credits xuống shop (kế thừa) |
| GET | /agent/shops, /agent/players /agent/* | fetchShops, fetchAgentPlayers | Dùng bởi ShopsPage/ShopDetailPage |
| PATCH | /agent/settings/low-balance-warning /agent/* | patchAgentLowBalanceWarning | Cảnh báo số dư thấp |
| GET | /agent/broadcasts, /agent/in-app-popups/* /agent/* | broadcasts / popups | Thông báo (kế thừa) |
Cookbook cho dev mới
Các tác vụ phổ biến và cách làm đúng theo quy ước project.
- Thêm hàm export vào
src/services/api.ts, gọiclient.get/post/patch/put(...)(token tự gắn). - Khai báo kiểu response trong
src/types/index.ts(theo envelope{ success, data?, message? }). - Component import hàm, tự quản
loading/errorbằnguseState; báo lỗi quatoast.
- Tạo page trong
src/pages/, bọc nội dung trong<AppLayout>. - Khai báo
<Route>trongApp.tsx, bọc<ProtectedRoute>nếu cần đăng nhập. - Thêm link vào
NAV_ITEMStrongcomponents/layout/AppLayout.tsx(đặtprimary:truenếu muốn hiện ở bottom nav mobile).
- Thêm giá trị vào mảng
AGENT_TABS+ cập nhậtisAgentDetailTab(). - Thêm nút tab (gọi
setActiveTab) và khối render{activeTab === '...' && (...)}. - Nếu tab cần fetch riêng, dùng
useEffectphụ thuộc[activeTab, agentId]+ cờcancelledđể tránh race.
- Backend lưu rate thập phân 0–1 (0.05 = 5%). UI nhập %.
- % → rate:
toRate()/percentInputToRate()(clamp 0–100,/100, toFixed(4)). - rate → %:
toPercent()(×100, toFixed(2)) hoặcformatCostRate()(Intl.NumberFormat style:'percent').
Bẫy thường gặp (đọc trước khi sửa)
| Bẫy | Chi tiết & cách xử lý |
|---|---|
| Tưởng đây là app riêng biệt | Là fork của Agent Portal: package.json name "kioskgaming-agent", README ghi "Agent". Type/hook tiền tố Agent* phục vụ SA. |
| Clash localStorage token | STORAGE_KEYS = agent_auth_token/agent_user_data dùng chung với Agent app. Chạy cùng origin sẽ ghi đè nhau — tách port (SA 3003). |
| Trông đợi refresh token | Login trả refreshToken nhưng app KHÔNG lưu/dùng. 401 = logout ngay. Không có cơ chế gia hạn. |
| Sai HTTP verb | suspend/unsuspend/terminate/rates/move dùng PATCH; create & credits dùng POST; game-mappings dùng PUT. Lấy verb từ api.ts, đừng tin bảng tổng quan. |
| Sai tên endpoint | Thực tế: /super-agent/shops/stats, /super-agent/agents/{id}/rates, .../credits/distribute, danh mục game /game-providers. |
| Prefix kép /agent vs /super-agent | Nhiều tính năng kế thừa vẫn gọi /agent/* (shops, players, broadcasts, popups, credits→shop, settings). Backend phải nhận token SA cho cả hai. |
| Chi tiết agent tải chậm | ManagedAgentDetailPage fetch toàn bộ 3 list rồi lọc client. Không có GET /super-agent/agents/:id. |
| Section ≠ tab | /agents /shops /players là 3 route khác nhau (cùng component), KHÔNG phải tab. Chỉ chi tiết agent mới có tab (?tab=). |
| SW không chạy khi dev | SW chỉ register ở production. Muốn test PWA phải npm run build + serve build/. |
| Cost rate đơn vị | Backend = thập phân 0–1; UI = %. Luôn dùng toRate/toPercent. Nhập 5 → lưu 0.0500. |
| Login profile key | Đọc res.data.agent HOẶC res.data.superAgent — backend trả khác key tuỳ phiên bản. |
| Captcha bật/tắt theo env | Turnstile chỉ bắt buộc khi REACT_APP_TURNSTILE_ENABLED='true' & có site key. Quên cấu hình sẽ chặn login khi bật. |