OrderDetailView.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import {
  2. DataUsage,
  3. OrderDetail,
  4. OrderHistory,
  5. } from "../../services/product/type";
  6. import { productApi } from "../../apis/productApi";
  7. import { startLoading, stopLoading } from "../../features/loading/loadingSlice";
  8. import { useAppDispatch } from "../../hooks/useRedux";
  9. import { useMutation } from "@tanstack/react-query";
  10. import React, { useState, useEffect } from "react";
  11. import { useLocation, useNavigate } from "react-router-dom";
  12. import { openQRModal } from "../../features/popup/popupSlice";
  13. import {
  14. convertOrderStatusToColor,
  15. convertOrderStatusToText,
  16. formatCurrency,
  17. formatNumber,
  18. } from "../../logic/loigicUtils";
  19. import { useTranslation } from "react-i18next";
  20. import { format } from "path";
  21. const OrderDetailView = () => {
  22. const location = useLocation();
  23. const dispatch = useAppDispatch();
  24. const navigate = useNavigate();
  25. const { t } = useTranslation();
  26. const [activeTab, setActiveTab] = useState<"detail" | "manage">("detail");
  27. const [orderDetails, setOrderDetails] = useState<OrderDetail[] | []>([]);
  28. const [dataUsage, setDataUsage] = useState<DataUsage[] | []>([]);
  29. const state = location.state as {
  30. id: number;
  31. orderHistory: OrderHistory;
  32. };
  33. useEffect(() => {
  34. getOrderDetailMutation.mutate();
  35. }, []);
  36. const getOrderDetailMutation = useMutation({
  37. mutationFn: async () => {
  38. dispatch(startLoading({}));
  39. const res = await productApi.getOrderDetail({
  40. orderId: state.id,
  41. });
  42. return res;
  43. },
  44. onSuccess: (data) => {
  45. dispatch(stopLoading());
  46. console.log("Get order detail response data:", data);
  47. if (data && data.errorCode === "0") {
  48. console.log("Get order detail successful");
  49. setOrderDetails(data.data ?? []);
  50. } else {
  51. console.error("Get order detail failed, no token received");
  52. }
  53. },
  54. onError: (error: any) => {
  55. dispatch(stopLoading());
  56. console.error("Get order detail error:", error.response.data);
  57. },
  58. });
  59. const handleTabChange = async (tab: "detail" | "manage") => {
  60. if (tab === "manage") {
  61. // Load manage data if needed
  62. dispatch(startLoading({}));
  63. try {
  64. const results = await Promise.all(
  65. orderDetails.map(async (element) => {
  66. const res = await productApi.checkDataUsage({
  67. iccid: element?.iccid,
  68. });
  69. if (res && res.errorCode === "0") {
  70. return res.data;
  71. }
  72. return null;
  73. }),
  74. );
  75. const validResults = results.filter(Boolean);
  76. setDataUsage(validResults);
  77. console.log("All data usage results:", validResults);
  78. } catch (error) {
  79. console.error("Check data usage error:", error);
  80. } finally {
  81. dispatch(stopLoading());
  82. }
  83. }
  84. setActiveTab(tab);
  85. };
  86. // Circular Progress Component
  87. const CircularProgress = ({ dataUsage }: { dataUsage: DataUsage }) => {
  88. const size = 180;
  89. const strokeWidth = 14;
  90. const radius = (size - strokeWidth) / 2;
  91. const circumference = 2 * Math.PI * radius;
  92. // 240 degrees calculation
  93. const arcAngle = 240;
  94. const arcLength = (arcAngle / 360) * circumference;
  95. const percentage = Math.min(
  96. Math.max(dataUsage.usageData / dataUsage.totalData, 0),
  97. 1,
  98. );
  99. const strokeDashoffset = arcLength - percentage * arcLength;
  100. // Rotate the SVG to center the 240-degree gap at the bottom
  101. // The gap is 120 degrees. We want it balanced.
  102. // Default SVG circle starts at 3 o'clock.
  103. // To have the 240 arc centered at top, we rotate.
  104. const rotation = 150; // (360 - 240) / 2 + 90
  105. return (
  106. <div className="flex flex-col items-center">
  107. <div className="flex flex-col items-center justify-center">
  108. <span className="text-sm md:text-sm font-black text-[#000] mb-2">
  109. {dataUsage.expiredTime
  110. ? `${t("expiredAt")}: ${dataUsage.expiredTime}`
  111. : ""}
  112. </span>
  113. </div>
  114. <div className="relative" style={{ width: size, height: size }}>
  115. <svg
  116. width={size}
  117. height={size}
  118. style={{ transform: `rotate(${rotation}deg)` }}
  119. className="overflow-visible"
  120. >
  121. {/* Background Path (240 degrees) */}
  122. <circle
  123. cx={size / 2}
  124. cy={size / 2}
  125. r={radius}
  126. fill="none"
  127. stroke="#f0f0f0"
  128. strokeWidth={strokeWidth}
  129. strokeLinecap="round"
  130. strokeDasharray={`${arcLength} ${circumference}`}
  131. />
  132. {/* Progress Path (240 degrees) */}
  133. <circle
  134. cx={size / 2}
  135. cy={size / 2}
  136. r={radius}
  137. fill="none"
  138. stroke="#EE0434"
  139. strokeWidth={strokeWidth}
  140. strokeLinecap="round"
  141. strokeDasharray={`${arcLength} ${circumference}`}
  142. strokeDashoffset={strokeDashoffset}
  143. className="transition-all duration-1000 ease-out"
  144. />
  145. </svg>
  146. {/* Central Text */}
  147. <div className="absolute inset-0 flex flex-col items-center justify-center">
  148. {dataUsage.totalData === 0 ? (
  149. <span className="text-3xl md:text-4xl font-black text-[#EE0434]">
  150. Unlimited
  151. </span>
  152. ) : (
  153. <span className="text-3xl md:text-4xl font-black text-[#EE0434]">
  154. {formatNumber(dataUsage.usageData)}
  155. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  156. </span>
  157. )}
  158. </div>
  159. {/* Min Label (Approximate position for 240deg start) */}
  160. {dataUsage.totalData === 0 ? null : (
  161. <>
  162. <div
  163. className="absolute text-xl font-bold font-black text-[#000000]"
  164. style={{ bottom: "5%", left: "12%" }}
  165. >
  166. {formatNumber(0)}
  167. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  168. </div>
  169. {/* Max Label (Approximate position for 240deg end) */}
  170. <div
  171. className="absolute text-xl font-bold font-black text-[#000000]"
  172. style={{ bottom: "5%", right: "8%" }}
  173. >
  174. {formatNumber(dataUsage.totalData)}
  175. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  176. </div>
  177. </>
  178. )}
  179. </div>
  180. <p className="mt-1 text-slate-400 text-sm font-bold uppercase tracking-widest">
  181. {dataUsage.status === 0
  182. ? t("notActive")
  183. : dataUsage.status === 1
  184. ? t("active")
  185. : dataUsage.status === 2
  186. ? t("finished")
  187. : dataUsage.status === 3
  188. ? t("expired")
  189. : t("unknown")}
  190. </p>
  191. {/* <p className="mt-1 text-slate-400 text-sm font-bold uppercase tracking-widest">
  192. {100 - Math.round((used / total) * 100)}% {t("remaining")}
  193. </p> */}
  194. </div>
  195. );
  196. };
  197. return (
  198. <div className="bg-[#fcfdfe] min-h-screen pb-20">
  199. {/* Breadcrumb */}
  200. <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
  201. <nav className="flex items-center space-x-2 text-xs md:text-sm text-slate-500 font-medium">
  202. <button
  203. // onClick={() => onViewChange(ViewMode.HOME)}
  204. className="hover:text-[#EE0434] text-[16px]"
  205. >
  206. {t("home")}
  207. </button>
  208. <svg
  209. className="w-3 h-3"
  210. fill="none"
  211. stroke="currentColor"
  212. viewBox="0 0 24 24"
  213. >
  214. <path
  215. d="M9 5l7 7-7 7"
  216. strokeWidth={2.5}
  217. strokeLinecap="round"
  218. strokeLinejoin="round"
  219. />
  220. </svg>
  221. <button
  222. // onClick={() => onViewChange(ViewMode.ORDER_HISTORY)}
  223. className="hover:text-[#EE0434] text-[16px] font-bold"
  224. >
  225. {t("myEsim")}
  226. </button>
  227. </nav>
  228. </div>
  229. <div className="max-w-5xl mx-auto px-4 py-8">
  230. {/* Back Button */}
  231. <button
  232. onClick={() => navigate(-1)}
  233. className="mb-6 flex items-center space-x-2 px-4 py-2 bg-white border border-slate-200 rounded-lg text-slate-600 font-bold hover:bg-slate-50 transition-colors shadow-sm"
  234. >
  235. <svg
  236. className="w-4 h-4"
  237. fill="none"
  238. stroke="currentColor"
  239. viewBox="0 0 24 24"
  240. >
  241. <path
  242. strokeLinecap="round"
  243. strokeLinejoin="round"
  244. strokeWidth={2}
  245. d="M10 19l-7-7 7-7m8 14l-7-7 7-7"
  246. />
  247. </svg>
  248. <span>{t("back")}</span>
  249. </button>
  250. {/* Main Info Card */}
  251. <div className="bg-white rounded-2xl border border-[#00b0f0]/30 shadow-sm overflow-hidden mb-8">
  252. <div className="p-6">
  253. {/* Header */}
  254. <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
  255. <div className="flex items-center gap-4">
  256. <div className="w-10 h-10 bg-[#00b0f0] rounded-xl flex items-center justify-center text-white shadow-sm">
  257. <svg
  258. className="w-6 h-6"
  259. fill="none"
  260. stroke="currentColor"
  261. viewBox="0 0 24 24"
  262. >
  263. <path
  264. strokeLinecap="round"
  265. strokeLinejoin="round"
  266. strokeWidth={2}
  267. d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"
  268. />
  269. </svg>
  270. </div>
  271. <span className="text-xl md:text-2xl font-bold text-slate-800">
  272. {state.orderHistory?.orderCode}
  273. </span>
  274. </div>
  275. <span
  276. className={`px-3 py-1 rounded-md text-sm font-bold shadow-sm ${convertOrderStatusToColor(state.orderHistory?.status)}`}
  277. >
  278. {convertOrderStatusToText(state.orderHistory?.status)}
  279. </span>
  280. </div>
  281. {/* Tabs */}
  282. <div className="flex space-x-1 mb-8">
  283. <button
  284. onClick={() => handleTabChange("detail")}
  285. className={`px-6 py-2 rounded-lg font-bold text-sm transition-colors text-[16px] ${
  286. activeTab === "detail"
  287. ? "bg-[#EE0434] text-white"
  288. : "text-[#EE0434] hover:bg-[#EE0434]/10"
  289. }`}
  290. >
  291. {t("detailDetail")}
  292. </button>
  293. <button
  294. onClick={() => handleTabChange("manage")}
  295. className={`px-6 py-2 rounded-lg font-bold text-sm transition-colors text-[16px] ${
  296. activeTab === "manage"
  297. ? "bg-[#EE0434] text-white"
  298. : "text-[#EE0434] hover:bg-[#EE0434]/10"
  299. }`}
  300. >
  301. {t("manageManage")}
  302. </button>
  303. </div>
  304. {/* Divider with Total */}
  305. <div className="relative flex items-center justify-center border-t border-slate-100 py-8">
  306. <span className="bg-white px-4 text-lg md:text-xl font-bold text-slate-800 absolute">
  307. {t("totalTotal")}:{" "}
  308. {formatCurrency(
  309. state.orderHistory?.paymentMoney,
  310. state.orderHistory?.curency,
  311. )}{" "}
  312. {/* <span className="text-slate-500 font-normal">
  313. ({state.orderHistory?.curency})
  314. </span> */}
  315. </span>
  316. </div>
  317. {/* Customer Info Grid */}
  318. <div className="grid grid-cols-1 md:grid-cols-4 gap-6 md:gap-4 mt-4">
  319. <div className="flex flex-col">
  320. <div className="flex items-center gap-2 text-slate-400 mb-1">
  321. <svg
  322. className="w-4 h-4"
  323. fill="none"
  324. stroke="currentColor"
  325. viewBox="0 0 24 24"
  326. >
  327. <path
  328. strokeLinecap="round"
  329. strokeLinejoin="round"
  330. strokeWidth={2}
  331. d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
  332. />
  333. </svg>
  334. <span className="text-xs font-bold text-[16px]">
  335. {t("fullName")}
  336. </span>
  337. </div>
  338. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  339. {state?.orderHistory.customerInfo.surName}{" "}
  340. {state?.orderHistory.customerInfo.lastName}
  341. </span>
  342. </div>
  343. <div className="flex flex-col">
  344. <div className="flex items-center gap-2 text-slate-400 mb-1">
  345. <svg
  346. className="w-4 h-4"
  347. fill="none"
  348. stroke="currentColor"
  349. viewBox="0 0 24 24"
  350. >
  351. <path
  352. strokeLinecap="round"
  353. strokeLinejoin="round"
  354. strokeWidth={2}
  355. d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
  356. />
  357. </svg>
  358. <span className="text-xs font-bold text-[16px]">
  359. {t("phoneNumber")}
  360. </span>
  361. </div>
  362. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  363. {state?.orderHistory.customerInfo.phoneNumber}
  364. </span>
  365. </div>
  366. <div className="flex flex-col">
  367. <div className="flex items-center gap-2 text-slate-400 mb-1">
  368. <svg
  369. className="w-4 h-4"
  370. fill="none"
  371. stroke="currentColor"
  372. viewBox="0 0 24 24"
  373. >
  374. <path
  375. strokeLinecap="round"
  376. strokeLinejoin="round"
  377. strokeWidth={2}
  378. d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
  379. />
  380. </svg>
  381. <span className="text-xs font-bold text-[16px]">Email</span>
  382. </div>
  383. <span className="text-slate-800 font-semibold text-sm text-[16px] break-all">
  384. {state?.orderHistory.customerInfo.email}
  385. </span>
  386. </div>
  387. <div className="flex flex-col">
  388. <div className="flex items-center gap-2 text-slate-400 mb-1">
  389. <svg
  390. className="w-4 h-4"
  391. fill="none"
  392. stroke="currentColor"
  393. viewBox="0 0 24 24"
  394. >
  395. <path
  396. strokeLinecap="round"
  397. strokeLinejoin="round"
  398. strokeWidth={2}
  399. d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 00-3 3z"
  400. />
  401. </svg>
  402. <span className="text-xs font-bold text-[16px]">
  403. {t("paymentMethod")}
  404. </span>
  405. </div>
  406. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  407. QR Code
  408. </span>
  409. </div>
  410. </div>
  411. </div>
  412. </div>
  413. {/* Product Section */}
  414. <div className="space-y-4">
  415. {orderDetails.length > 0 && (
  416. <div className="flex items-center space-x-3">
  417. <img
  418. src={`${orderDetails[0].areaIconUrl}`}
  419. className="w-8 h-8 rounded-full object-cover border border-slate-200"
  420. />
  421. <h3 className="text-lg font-bold text-slate-900">
  422. SIM {orderDetails[0].areaName}
  423. </h3>
  424. </div>
  425. )}
  426. <div
  427. className={`rounded-2xl border border-slate-100 shadow-sm p-6 relative ${
  428. activeTab === "manage" ? "bg-slate-50/50" : "bg-white"
  429. }`}
  430. >
  431. {activeTab === "detail" ? (
  432. // DETAIL TAB CONTENT
  433. <div className="space-y-8">
  434. {orderDetails.map((pkg, idx) => (
  435. <div
  436. key={idx}
  437. className={`flex flex-col md:flex-row justify-between items-start md:items-center ${
  438. idx !== orderDetails.length - 1
  439. ? "border-b border-slate-100 pb-8"
  440. : ""
  441. }`}
  442. >
  443. <div className="space-y-2">
  444. <div className="flex items-center gap-2">
  445. <h4 className="text-xl font-bold text-slate-800">
  446. {pkg.packageName}
  447. </h4>
  448. {/* <span className="px-2 py-0.5 rounded bg-slate-100 text-slate-600 text-xs font-bold">
  449. {pkg.id}
  450. </span> */}
  451. </div>
  452. {/* <span className="inline-block bg-[#ff0050] text-white text-[10px] font-bold px-2 py-0.5 rounded">
  453. TikTok
  454. </span> */}
  455. <p className="text-slate-500 text-sm font-bold">
  456. {t("validityPeriod")}: {pkg.dayDuration} {t("days")}
  457. </p>
  458. </div>
  459. <div className="mt-4 md:mt-0 flex flex-col items-end">
  460. <button
  461. className="flex items-center text-[#EE0434] font-bold text-sm mb-2 hover:underline"
  462. onClick={() => {
  463. dispatch(openQRModal(pkg.qrcodeUrl));
  464. }}
  465. >
  466. <svg
  467. className="w-4 h-4 mr-1"
  468. fill="none"
  469. stroke="currentColor"
  470. viewBox="0 0 24 24"
  471. >
  472. <path
  473. strokeLinecap="round"
  474. strokeLinejoin="round"
  475. strokeWidth={2}
  476. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  477. />
  478. <path
  479. strokeLinecap="round"
  480. strokeLinejoin="round"
  481. strokeWidth={2}
  482. d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
  483. />
  484. </svg>
  485. QR Code
  486. </button>
  487. <div className="text-right">
  488. <span className="text-xl font-bold text-slate-800">
  489. {formatCurrency(pkg.paymentMoney, pkg.curency)}
  490. </span>
  491. {/* <span className="text-slate-500 font-medium ml-1 font-bold">
  492. {" "}
  493. đ
  494. </span> */}
  495. </div>
  496. </div>
  497. </div>
  498. ))}
  499. </div>
  500. ) : (
  501. // MANAGE TAB CONTENT - Grid of Circular Progress
  502. <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  503. {dataUsage.map((pkg, idx) => (
  504. <div
  505. key={idx}
  506. className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm flex flex-col items-center hover:shadow-md transition-shadow"
  507. >
  508. <h4 className="text-lg font-bold text-slate-800 mb-2">
  509. {orderDetails[idx].packageName}
  510. </h4>
  511. <CircularProgress dataUsage={pkg} />
  512. </div>
  513. ))}
  514. </div>
  515. )}
  516. </div>
  517. </div>
  518. </div>
  519. </div>
  520. );
  521. };
  522. export default OrderDetailView;