Admin · Deep dive

Technical details —Admin Dashboard

Dive into how the internal dashboard executes: CRA bootstrap, React Router + ProtectedRoute, hooksuseAuthnot context, layerapiClientaxios wrapper, React Query class, 3-step withdrawal flow, and other "real code is different from documentation" points. Read the following overview report03-admin.html.

React 18.2 TypeScript 4.9 CRA · react-scripts 5 React Query v3 React Router v6 Tailwind 3

Stack & holistic thinking

Before reading the code, understand the six characteristics that determine how this app is organized. This is an internal business tool (operations, finance) — different from the player-oriented frontend kiosk.

🏗️

CRA + CRACO, alias @/

craco start / craco build. Webpack alias @src/. Shared package @kioskgaming/ui + @kioskgaming/page-loading. Dev default port CRA3000(no more hardcode 3002).

🧭

Navigate using the real URL

In contrast to kiosk frontend (state machine in-memory), admin usesReact Router v6with real URL route:/dashboard, /users/:id, /withdrawal-requests… Each wrapped routeProtectedRoute.

🔌

axios + React Query, no manual fetching

A singletonapiClientwrap axios (interceptor token + 401). All server reads/writes go through the internal React Query v3 hookhooks/useApi.ts — cache, refetch, mutation.

🔐

useAuthis Context (AuthProvider)

App wrap<AuthProvider>— State auth synchronizes throughout the app.logout()clear React Query cache. Permissions fromresolveUserPermissions()(API objects orROLE_PERMISSIONS map).

🪟

UI = modal-per-action

Each sensitive action (approve / reject / complete / ban / update status) has a separate modal + confirmation. Repeat pattern:useState for selected*+ flagshow*Modal.

🎨

Tailwind 3 + shadcn style theme

Havetailwind.config.jsmap to internal HSL variableindex.css (--primary, --card…). Already defined.darkButThere is no button to turn on dark mode.

💡
Read in what order?

Newbies should open:src/App.tsx(route map) →components/auth/ProtectedRoute.tsx + hooks/useAuth.ts(gatekeeping & decentralization) →services/api.ts(how to call backend) →hooks/useApi.ts(React Query) → a feature, for examplecomponents/pages/withdrawalRequest/. These 5 files are ~70% of the system.

Directory structuresrc/

Organized by "feature folder": each business is an internal foldercomponents/pages/withindex.tsxcoordinate + child Lists/Details/Modals. Import usedrelative path(no alias@/).

src/ ├── index.tsx # Entry: ReactDOM.createRoot + StrictMode ├── App.tsx # ★ QueryClient + Router + entire <Route> ├── index.css # @tailwind + HSL variable :root/.dark (shadcn-style) ├── components/ │ ├── auth/ │ │ ├── ProtectedRoute.tsx # ★ Gatekeeping auth + permission │ │ └── LoginPage.tsx # 2-step login (password → email code) │ ├── layout/ # Layout, Sidebar, Header, MenuContainer │ ├── pages/ │ │ ├── Dashboard.tsx # 6 stat cards + 4 summary panels │ │ ├── users/ # UserList, UserDetailPage, Wallet*, Ban/Status modal │ │ ├── withdrawalRequest/ # index + List/Details + Approve/Reject/Complete modal │ │ ├── transactions/ # List, Details, UpdateStatus modal │ │ └── tickets/ # List, Details, Reply/UpdateStatus modal │ └── ui/ # Card, Badge, Table, Button, Input, Loading, Menu ├── hooks/ │ ├── useAuth.ts # ★ Auth state (hook, no context) + RBAC │ └── useApi.ts # ★ Complete React Query hook (query + mutation) ├── services/ api.ts # ★ Singleton apiClient wraps axios ├── types/ index.ts # ~700 interface lines for all entities/responses ├── constants/ index.ts # API_BASE_URL, ENDPOINTS, STORAGE_KEYS, messages… └── utils/ index.ts # cn(), formatCurrency, formatDate, debounce…
🗂️
Each feature folder has files*_API.md

In each page folder (egwithdrawalRequest/WITHDRAWAL_REQUEST_API.md, users/USER_MANAGEMENT_API.md) has accompanying API documentation. Convenient to look up, butAlways compare with real codebecause of some endpoints in the document/constantsmisaligned (see Traps section).

Bootstrap & provider tree

What happens when the browser opens the app. Other kiosk frontend, here there isQueryClientProviderall inclusive andBrowserRoutercoordinate by URL.

1
index.tsx mount React

ReactDOM.createRoot(#root).render(<StrictMode><App/></StrictMode>). StrictMode is on → in dev each effect runs twice.

2
App.tsx builds provider

QueryClientProvider (retry 1, refetchOnWindowFocus:false) → BrowserRouter<Routes> + <Toaster>of react-hot-toast (top-right corner).

3
ProtectedRoute checks the session

Each route is privateProtectedRoute. It callsuseAuth()→ readlocalStorage. DuringisLoadingdisplay<LoadingPage>"Verifying...".

Render Layout + trang

Have a valid token → render<Layout>(Sidebar + Header + main) wraps the page content. No tokens →<Navigate to="/login">.

src/App.tsx (shortened)tsx
const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
});

return (
  <QueryClientProvider client={queryClient}>
    <Router>
      <Routes>
        <Route path="/login" element={<LoginPage/>} />
        <Route path="/users" element={
          <ProtectedRoute requiredPermission="userManagement">
            <Layout><UsersManagement/></Layout>
          </ProtectedRoute>} />
        // … catch-all → Navigate to="/dashboard"
      </Routes>
      <Toaster position="top-right" />
    </Router>
  </QueryClientProvider>
)

Route table &ProtectedRoute

All routes declared flat inApp.tsx(no nested layout routes, no lazy loading). Each private route is wrapped in 2 layers:ProtectedRoute(guard) +Layout (khung UI).

URLComponentrequiredPermissionNote
/loginLoginPagepublic2-step login, redirect if already logged in
/NavigateWrap Layout, redirect→ /dashboard
/dashboardDashboard— (just login)6 stat card + 4 panel
/usersUsersManagementuserManagementList + filter
/users/:idUserDetailPageuserManagementDetails + wallet + ban/status
/transactionsTransactionsManagementtransactionManagementEvery underlying transaction
/withdrawal-requestsWithdrawalManagementwithdrawalManagementBrowse withdrawals in 3 steps
/ticketsTicketManagementticketManagementCustomer support
/walletsJSX inline placeholderwalletManagement"Under development..." right in App.tsx
/adminsJSX inline placeholderadminManagement"Under development..." right in App.tsx
*NavigateCatch-all → /dashboard
src/components/auth/ProtectedRoute.tsxtsx
if (isLoading) return <LoadingPage message="Verifying..." />;

if (!isAuthenticated)
  return <Navigate to="/login" state={{ from: location }} replace />;

if (requiredRole && !hasRole(requiredRole))
  return <Navigate to="/dashboard" replace />;

if (requiredPermission && !hasPermission(requiredPermission))
  return <Navigate to="/dashboard" replace />;

return <>{children}</>;
🧭
Sidebar & NAVIGATION_ITEMSdifferent

Layout.tsxHardcode is correct5sidebar items (Dashboard, Users, Transactions, Withdrawal Requests, Support Tickets) —Are notthere are Wallets/Admins. Whileconstants.NAVIGATION_ITEMSlist7entry and also states the withdrawal path is/withdrawals(what a route/withdrawal-requests). This constant doesnot usedto render the sidebar.

Auth & RBAC — AuthProvider + permissions.ts

useAuthis React Context viaAuthProvider(wrapped inApp.tsx). Permissions resolve frompermissionObjectsAPI orROLE_PERMISSIONSmap with 5 main roles.

localStorage useEffect reads token+userData authState hasPermission()

2-step login (send code → login)

1
Enter email + password

useSendEmailCode()POST /api/auth/send-code { email, password }. Success → move forwardcode, turn on the 60s countdown.

2
Enter the 6-digit code from email

useLogin()POST /api/auth/login { email, password, emailCode }.

3
Save token + user

onSuccessbelong touseLoginWrite straightadmin_auth_token & admin_user_dataGo to localStorage (not throughuseAuth.login()).

Navigate to app

navigate(from). Khi ProtectedRoute mount, useAuthRead localStorage again → see logged in.

Decentralization (RBAC) — hardcode by role

src/hooks/useAuth.ts — hasPermissionts
if (!authState.user) return false;

// Admin has ALL rights
if (user.profile?.role === 'admin') return true;

// Finance only has 4 financial rights
if (user.profile?.role === 'finance') {
  const financePermissions = [
    'walletManagement', 'transactionManagement',
    'withdrawalManagement', 'financeManagement',
  ];
  return financePermissions.includes(permission);
}
return false; // other roles → no rights
PermissionadminfinanceOther
userManagement
walletManagement
transactionManagement
withdrawalManagement
financeManagement
ticketManagement
adminManagement
🐛
3 easy points to stumble about auth

(1) AuthProvidersync state throughout the app;logout()callqueryClient.clear(). (2) RBAC client-only — backend authorize JWT. (3) Roles: system_admin, super_admin, platform_admin, finance_admin, support_admin— no longer just admin/finance.

apiClient— singleton wrapped axios

A classApiClientmade availableaxios.create()with base URL + 10s timeout, attach 2 interceptors and expose ~25 methods corresponding to each backend endpoint. Export 1 instanceapiClientshared throughout the app.

PATTERNSingleton + axios instance + interceptors
  • Base URL: API_BASE_URL = 'http://localhost:3001/api' — hardcode in constants, all paths include a prefix/api
  • Request interceptor:readlocalStorage.getItem('admin_auth_token')→ addAuthorization: Bearer <token>
  • Response interceptor:If401→ deleteadmin_auth_token + admin_user_datawindow.location.href = '/login' (hard redirect)
  • Do not refresh tokens:401 means logging out, there is no refresh mechanism like kiosk frontend
  • Dense log:Each method has manyconsole.logrequest/response/error (debug left)
src/services/api.ts (shortened)ts
this.client = axios.create({ baseURL: API_BASE_URL, timeout: 10000 });

this.client.interceptors.request.use((config) => {
  const token = localStorage.getItem('admin_auth_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

this.client.interceptors.response.use(r => r, (error) => {
  if (error.response?.status === 401) {
    localStorage.removeItem('admin_auth_token');
    window.location.href = '/login';
  }
  return Promise.reject(error);
});

Endpoint maps (all under prefix/api)

GroupMethod apiClientHTTP & real endpoint
AuthsendEmailCode, loginPOST /auth/send-code, /auth/login
DashboardgetDashboardStatsGET /admin/dashboard/stats
UsersgetUsers, getUserDetails, banUser, unbanUser, updateUserStatusGET /admin/users, /admin/users/:id · POST …/ban, …/unban · PATCH …/status
WalletgetUserWallet, getWalletTransactionsGET /admin/users/:id/wallet, …/wallet/transactions
TransactionsgetTransactions, getTransactionStats, getTransactionDetail, updateTransactionStatusGET /admin/transactions, …/stats, …/:id · PATCH …/:id/status
WithdrawalsgetWithdrawals, approve/reject/completeGET /payment/admin/withdrawals/pending · POST /payment/admin/withdrawals/:id/{approve,reject,complete}
TicketsgetTickets, getTicket, replyTicket, updateTicketStatus, markTicketDone, getSupportStats, getCategoriesGET /support/admin/tickets, /support/tickets/:num, /support/admin/stats, /support/categories · POST …/reply · PATCH …/:num, …/:num/done
📍
constants.ENDPOINTSoutdated — don't trust it

Object ENDPOINTS in constants/index.ts impossible api.tsuse (api.ts write path literal). Many values ​​are skewed:ENDPOINTS.USERS = '/users'but really/admin/users; ENDPOINTS.TRANSACTIONS = '/payment/transactions'but really/admin/transactions. When adding an endpoint, edit it directly inapi.ts.

React Query class —hooks/useApi.ts

Component Are notcallapiClientdirect. They use internal React Query hooksuseApi.ts: useQueryfor reading,useMutationfor recording. Mutation automatically invalidates/refetchs related queries and fires a toast.

useQuery (read)
  • useDashboardStats — staleTime 5′, refetchInterval 30s
  • useUsers(filters), useUserDetails(id)keepPreviousData
  • useTransactions, useTransactionStats, useTransactionDetail
  • useWithdrawals(filters) — staleTime 2′
  • useTickets, useTicket, useSupportStats, useCategories
useMutation (ghi)
  • useSendEmailCode, useLogin (toast + ghi localStorage)
  • useBanUser, useUnbanUser, useUpdateUserStatus
  • useApproveWithdrawal, useRejectWithdrawal, useCompleteWithdrawal
  • useReplyTicket, useUpdateTicketStatus, useMarkTicketDone
  • useUpdateTransactionStatus
src/hooks/useApi.ts — query key + invalidate samplets
export const useUsers = (filters) =>
  useQuery(['users', filters], () => apiClient.getUsers(filters),
    { keepPreviousData: true, staleTime: 5 * 60 * 1000 });

export const useBanUser = () => {
  const qc = useQueryClient();
  return useMutation(({ userId, data }) => apiClient.banUser(userId, data), {
    onSuccess: () => {
      qc.invalidateQueries('users');
      qc.invalidateQueries('userDetails');
      toast.success(SUCCESS_MESSAGES.USER_BANNED);
    },
    onError: (e) => toast.error(parseErrorMessage(e)),
  });
};
🔁
The two invalidate styles coexist

The user/transaction hook uses the old React Query v3 syntax (invalidateQueries('users')+ toast in hook). The withdrawal/ticket hook uses the new object syntax (invalidateQueries({ queryKey: [...] })) andAre nottoast in hook — page handles refetching itself (egWithdrawalManagementtransmitonSuccesscallrefetch()). When adding hooks, follow the style of the feature being edited for consistency.

Type system & "envelope" API

The entire style is withintypes/index.ts(~700 lines). Most responses follow the standard "envelope".{ success, message, statusCode, timestamp, data }. This is where many "data shape traps" reside.

types/index.ts — envelope & typical entitiests
export interface ApiResponse<T> {
  success: boolean; message?: string;
  data: T; statusCode?: number; timestamp?: string;
}

export interface User {
  id: string; email: string; status: 'active'|'inactive'|'suspended';
  banned: boolean; profile: Record<string, any>; // role is in profile.role
}

// ⚠ NO envelope — only flat:
export interface WithdrawalsResponse {
  withdrawals: Withdrawal[]; pagination: Pagination;
}
🧩

rolelies withinprofile

Permission to read throughuser.profile?.role. ButUser.profiledeclareRecord<string, any>with caption "API returns {} empty object" → needs backend data setroleIn the right place, RBAC will run.

⚠️

DashboardStatsDeclare 2 times

Interface DashboardStatsbe definedcoincide(2 identical blocks) intypes/index.ts. Harmless because they're similar, but it's a sign of copy-paste that needs to be cleaned up.

🧨
Shape deviation:WithdrawalsResponsevs usage

Type of withdrawal response declaration is{ withdrawals, pagination }(flat), butwithdrawalRequest/index.tsxread againwithdrawalsData?.data?.withdrawalsand…?.data?.pagination— which means expecting more classesdata. BecauseuseWithdrawalsNo strict generic constraints so TS doesn't catch errors; The actual run must follow the returned JSON backend. When connecting the API, check the console.log response to see if the class is presentdataor not.

Withdrawal approval — 3 steps

The most important business, sample example for the entire "list + modal-per-action" architecture.index.tsxkeep state & coordination;WithdrawalListshow + filter; 3 modals execute each action.

pending approved completed rejected
StepHookHTTP & endpointStatus
See listuseWithdrawals(filters)GET /payment/admin/withdrawals/pendingpending
ApproveuseApproveWithdrawalPOST /payment/admin/withdrawals/:id/approveapproved
CompleteuseCompleteWithdrawalPOST /payment/admin/withdrawals/:id/completecompleted
RejectuseRejectWithdrawalPOST /payment/admin/withdrawals/:id/rejectrejected
withdrawalRequest/index.tsx — modal-per-action templatetsx
const [selectedWithdrawal, setSelectedWithdrawal] =
  useState<WithdrawalRequest | null>(null);
const [showApproveModal, setShowApproveModal] = useState(false);

const handleApproveWithdrawal = (w) => {
  setSelectedWithdrawal(w);
  setShowApproveModal(true);
};

// onSuccess of the modal → refetch() to update the list
const handleWithdrawalUpdate = () => refetch();
🔎

Filter ofWithdrawalList

Theo status (pending/approved/completed/rejected), withdrawalType(bank_transfer / bitcoin_transfer), anddateFrom/dateTo. Change filter → resetpage = 1. Default statelimit: 20.

🏦

2 types of withdrawals

Bank: bankCode, accountNumber, accountName, routingNumber. Bitcoin:wallet address. Finance team transfers money/crypto manually and then clicks Complete.

🧷
dateFrom/dateTonot in the type filter

WithdrawalFiltersjust declarestatus, page, limit, userId, withdrawalTypeAre notHavedateFrom/dateTo, umbrellaindex.tsxinitialize andWithdrawalListset these 2 fields. This is a harmless type deviation at compile time (due to the use of spread) but can easily cause confusion. If you need to filter standard dates, add 2 fields to the type.

User management & wallet

Same list + modal template.users/index.tsxholdfiltersand transmitted downUserList; UserDetailPageCombine user information + wallet + transaction history.

📋

UserList

Search by email, filterstatus & banned, sort by created_at/email/status/banned, pagination. Click row →navigate('/users/:id'). Each row has a Ban & Update Status opens the modal.

👤

UserDetailPage

Show info +stats (totalDeposits, totalWithdrawals, walletBalance, totalTransactions), WalletSection(manual credit/debit) andWalletTransactionTable(wallet history with filter + pagination).

ActionModalHookEndpoint
Ban accountBanUserModaluseBanUserPOST /admin/users/:id/ban { reason? }
UnblockBanUserModal(unban mode)useUnbanUserPOST /admin/users/:id/unban
Change statusUpdateUserStatusModaluseUpdateUserStatusPATCH /admin/users/:id/status
Deposit/deduct walletWalletSection(via apiClientwallet)credit/debit (amount + reason + description)
🔒
Ghi audit when ban

The response you returnedbanned_at, ban_reason(and recording backendbanned_by). Modal asks to enter a reason.status(active/inactive/suspended) is separate from flagsbanned— suspended is lighter than you.

Customer Support & transaction monitoring

The remaining two modules follow the same philosophy. Other points:tickets/index.tsxuse "master-detail" in the same page (select ticket → showTicketDetails) instead of URL navigation.

🎫 Tickets
  • TicketListfilter by status: pending / in_progress / resolved / closed / done
  • TicketDetailsDisplay thread reply (distinguish between admin/user)
  • ReplyTicketModal— with flagisInternal(internal notes)
  • UpdateTicketStatusModal + markTicketDone (ghi resolvedAt/By)
  • The list is taken from the pastuseCategories()
💳 Transactions
  • TransactionListfilter type/status/method/date + sort
  • TransactionDetails— raw payload (paymentResult, linkMePayRequest)
  • UpdateTransactionStatusModal — pending/completed/failed/cancelled
  • useTransactionStats — overview + byType + byMethod
  • Transaction ID isnumber (getTransactionDetail(id: number))
🔀
2 different ways to "open details".

User usesroute (/users/:id→ separate page). Tickets & Withdrawal usedinternal state(modal/panel). When adding features, choose the appropriate template: need deep-link/refresh to keep context → route; Quick action in list → modal.

Tailwind 3 & shadcn style theme

Unlike kiosk frontend (Tailwind v4 does not have config), admins use Tailwind 3tailwind.config.js+ PostCSS. Map color to HSL variable declared inindex.cssin shadcn/ui style.

src/index.css + tailwind.config.jscss
@tailwind base; @tailwind components; @tailwind utilities;

@layer base {
  :root {
    --primary: 221.2 83.2% 53.3%;  /* blue */
    --card: 0 0% 100%; --border: 214.3 31.8% 91.4%;
    --radius: 0.5rem;
  }
  .dark { --background: 222.2 84% 4.9%; /* …defined but not used yet */ }
}

// tailwind.config.js → colors: { primary: 'hsl(var(--primary))', card: 'hsl(var(--card))' … }

Component UI

Setui/Write yourself in shadcn style:Card, Badge, Button (variant default/outline/ghost), Input, Table, Loading, Menu. Merge class withcn() (twMerge(clsx())).

Icon & toast

Icon lucide-react. Notificationreact-hot-toast (<Toaster>In App, style according to card/border variable). Dashboard chart usedrecharts.

Layout

Layout= Sidebar (fixed, collapsed mobile) + Header + main scroll. Page title inferred fromlocation.pathname in getPageTitle().

🧹
Debug UI left in sidebar

Layout.tsxprints a red line"Debug: ADMIN / NOT ADMIN"right below the user name in the footer sidebar. Along with seriesconsole.log in api.ts/useApi.ts/useAuth.ts, this is the debug code that needs to be cleaned up before production.

Config & environment variable

Configuration is centralizedconstants/index.ts. Worth noting: the backend base URL is brokenhardcode, does not read environment variablesREACT_APP_*as CRA standard.

ConstantValueMeaning
API_BASE_URLprocess.env.REACT_APP_API_BASE_URLDefaulthttp://localhost:3001/api— read from env viaconfig/env.ts
PORT3000 (CRA default)Dev server via craco start
STORAGE_KEYSadmin_auth_token, admin_user_data, admin_themeLocalStorage key
PAGINATION_DEFAULTS{ page:1, limit:10 }Default is constant (but pages set it themselveslimit:20)
CURRENCY{ symbol:'$', code:'USD' }formatCurrency render by en-US/USD
🌐
Changing the backend URL requires editing the code

BecauseENV.API_BASE_URLreadREACT_APP_API_BASE_URL, create.envfor other environments. Turnstile:REACT_APP_TURNSTILE_ENABLED + REACT_APP_TURNSTILE_SITE_KEYwhen the backend enables CAPTCHA login.

Testing & build

The test infrastructure is CRA's default (Jest + react-scripts) but the repoThere are no test files yet. Build/dev also follows CRA (Webpack), different from turbopack of kiosk frontend.

🧪

What's up?

10 unit test files (*.test.ts(x)) for utils, forgot-password, USD wallet mappers. @testing-library/react in deps. Playwright E2E: tests/visual/, scripts test:e2e, test:visual.

Scripts

npm start (craco, port 3000), npm run build (+ SPA fallback postbuild), npm test, npm run test:e2e (Playwright).

Scripts (package.json)bash
npm start    # PORT=3002 react-scripts start → http://localhost:3002
npm run build  # react-scripts build (Webpack) → build/
npm test     # react-scripts test (Jest, no tests yet)
npm run eject  # extract CRA config (not recommended)
🛡️
Other kiosk frontend: build DOES NOT ignore type errors

CRA react-scripts buildwillfailwhen there is a compile error (unlike Next withignoreBuildErrors). Ask for a favortsconfig strict, type errors are caught when building — but errorsany/spread (see type section) still gets through.

Cookbook for new devs

Common tasks follow project conventions.

HOW-TOAdd a new page/route (eg /reports)
  1. Create folderssrc/components/pages/reports/index.tsx (feature folder).
  2. More<Route> in App.tsx, wrap<ProtectedRoute requiredPermission="..."> + <Layout>.
  3. MoreSidebarItemrespectively inlayout/Layout.tsx(remember: sidebar is hardcode, not generated from constants).
  4. If there are new permissions, add an internal branchuseAuth.hasPermission.
HOW-TOCall a new backend API
  1. Add methods to the classApiClient in services/api.ts(usethis.client.get/post/patch, path literal has prefix/api).
  2. Declare the request/response interface intypes/index.ts (by envelope { success, data }).
  3. Create internal hookshooks/useApi.ts: useQuery(read) oruseMutation(write) + invalidate related query.
  4. Component calls hook; error reportedtoast.error(parseErrorMessage(e)).
HOW-TOAdd an action + modal (eg "Refund")
  1. CreateRefundModal.tsxin feature folder (getisOpen, onClose, onSuccess, entity is selected).
  2. Trong index.tsx: add stateselected* + showRefundModal+ handler set both.
  3. Modal calleduseRefund(); onSuccessclose modal +refetch()/invalidate.
  4. Add a modal open button in the List component.
HOW-TOPoint the app to another backend
  1. FixAPI_BASE_URL in src/constants/index.ts(or refactor toprocess.env.REACT_APP_API_URL+ create.env).
  2. Restart the dev server (CRA only reads env at start).
  3. Check Network/console: every request goes through an interceptor with a Bearer token attached.

Common traps (read before fixing)

TrapDetails & how to handle
useAuthis ContextAuthProviderwrap app;logout()clear query cache. Don't expect independent hooks like the old report.
Backend URL via envREACT_APP_API_BASE_URL in config/env.ts- create.env for staging/prod.
Sidebar ≠ NAVIGATION_ITEMSSidebar hardcodes 5 items inLayout.tsx; constant 7 entries with wrong withdrawal path (/withdrawals vs /withdrawal-requests). Add item to edit Layout.
Deviateddatain withdrawalsType WithdrawalsResponseFlat but readable code?.data?.withdrawals. Stick to real JSON when connecting to the API.
RBAC is only on the clientThe backend must authorize itself according to JWT. Roleoperation/platformnot yet inhasPermission.
Backend URL hardcodeAPI_BASE_URLpermanentlocalhost:3001/api, Not available.env. Changing the environment requires modifying the code + rebuilding.
Default currency USDformatCurrencyrender USD, transaction can be in VND. Checkcurrencyfrom data.
No testing yetnpm test"No tests found" message; Lack of Testing Library. Install before writing test components.
Debug remainingLine "Debug: ADMIN/NOT ADMIN" in sidebar + manyconsole.login api/hooks. Clean up before production.
StrictMode runs the effect twice (dev)In dev, effect/log runs doubleReact.StrictMode. Normal, not a bug.