OrderDetailView.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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. <span className="text-3xl md:text-4xl font-black text-[#EE0434]">
  149. {formatNumber(dataUsage.usageData)}
  150. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  151. </span>
  152. </div>
  153. {/* Min Label (Approximate position for 240deg start) */}
  154. <div
  155. className="absolute text-xl font-bold font-black text-[#000000]"
  156. style={{ bottom: "5%", left: "12%" }}
  157. >
  158. {formatNumber(0)}
  159. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  160. </div>
  161. {/* Max Label (Approximate position for 240deg end) */}
  162. <div
  163. className="absolute text-xl font-bold font-black text-[#000000]"
  164. style={{ bottom: "5%", right: "8%" }}
  165. >
  166. {formatNumber(dataUsage.totalData)}
  167. <span className="text-[16px]">{dataUsage.dataUnit}</span>
  168. </div>
  169. </div>
  170. <p className="mt-1 text-slate-400 text-sm font-bold uppercase tracking-widest">
  171. {dataUsage.status === 0
  172. ? t("notActive")
  173. : dataUsage.status === 1
  174. ? t("active")
  175. : dataUsage.status === 2
  176. ? t("finished")
  177. : dataUsage.status === 3
  178. ? t("expired")
  179. : t("unknown")}
  180. </p>
  181. {/* <p className="mt-1 text-slate-400 text-sm font-bold uppercase tracking-widest">
  182. {100 - Math.round((used / total) * 100)}% {t("remaining")}
  183. </p> */}
  184. </div>
  185. );
  186. };
  187. return (
  188. <div className="bg-[#fcfdfe] min-h-screen pb-20">
  189. {/* Breadcrumb */}
  190. <div className="max-w-7xl mx-auto px-4 py-4 md:py-6 border-b border-slate-50">
  191. <nav className="flex items-center space-x-2 text-xs md:text-sm text-slate-500 font-medium">
  192. <button
  193. // onClick={() => onViewChange(ViewMode.HOME)}
  194. className="hover:text-[#EE0434] text-[16px]"
  195. >
  196. {t("home")}
  197. </button>
  198. <svg
  199. className="w-3 h-3"
  200. fill="none"
  201. stroke="currentColor"
  202. viewBox="0 0 24 24"
  203. >
  204. <path
  205. d="M9 5l7 7-7 7"
  206. strokeWidth={2.5}
  207. strokeLinecap="round"
  208. strokeLinejoin="round"
  209. />
  210. </svg>
  211. <button
  212. // onClick={() => onViewChange(ViewMode.ORDER_HISTORY)}
  213. className="hover:text-[#EE0434] text-[16px] font-bold"
  214. >
  215. {t("myEsim")}
  216. </button>
  217. </nav>
  218. </div>
  219. <div className="max-w-5xl mx-auto px-4 py-8">
  220. {/* Back Button */}
  221. <button
  222. onClick={() => navigate(-1)}
  223. 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"
  224. >
  225. <svg
  226. className="w-4 h-4"
  227. fill="none"
  228. stroke="currentColor"
  229. viewBox="0 0 24 24"
  230. >
  231. <path
  232. strokeLinecap="round"
  233. strokeLinejoin="round"
  234. strokeWidth={2}
  235. d="M10 19l-7-7 7-7m8 14l-7-7 7-7"
  236. />
  237. </svg>
  238. <span>{t("back")}</span>
  239. </button>
  240. {/* Main Info Card */}
  241. <div className="bg-white rounded-2xl border border-[#00b0f0]/30 shadow-sm overflow-hidden mb-8">
  242. <div className="p-6">
  243. {/* Header */}
  244. <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
  245. <div className="flex items-center gap-4">
  246. <div className="w-10 h-10 bg-[#00b0f0] rounded-xl flex items-center justify-center text-white shadow-sm">
  247. <svg
  248. className="w-6 h-6"
  249. fill="none"
  250. stroke="currentColor"
  251. viewBox="0 0 24 24"
  252. >
  253. <path
  254. strokeLinecap="round"
  255. strokeLinejoin="round"
  256. strokeWidth={2}
  257. 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"
  258. />
  259. </svg>
  260. </div>
  261. <span className="text-xl md:text-2xl font-bold text-slate-800">
  262. {state.orderHistory?.orderCode}
  263. </span>
  264. </div>
  265. <span
  266. className={`px-3 py-1 rounded-md text-sm font-bold shadow-sm ${convertOrderStatusToColor(state.orderHistory?.status)}`}
  267. >
  268. {convertOrderStatusToText(state.orderHistory?.status)}
  269. </span>
  270. </div>
  271. {/* Tabs */}
  272. <div className="flex space-x-1 mb-8">
  273. <button
  274. onClick={() => handleTabChange("detail")}
  275. className={`px-6 py-2 rounded-lg font-bold text-sm transition-colors text-[16px] ${
  276. activeTab === "detail"
  277. ? "bg-[#EE0434] text-white"
  278. : "text-[#EE0434] hover:bg-[#EE0434]/10"
  279. }`}
  280. >
  281. {t("detailDetail")}
  282. </button>
  283. <button
  284. onClick={() => handleTabChange("manage")}
  285. className={`px-6 py-2 rounded-lg font-bold text-sm transition-colors text-[16px] ${
  286. activeTab === "manage"
  287. ? "bg-[#EE0434] text-white"
  288. : "text-[#EE0434] hover:bg-[#EE0434]/10"
  289. }`}
  290. >
  291. {t("manageManage")}
  292. </button>
  293. </div>
  294. {/* Divider with Total */}
  295. <div className="relative flex items-center justify-center border-t border-slate-100 py-8">
  296. <span className="bg-white px-4 text-lg md:text-xl font-bold text-slate-800 absolute">
  297. {t("totalTotal")}:{" "}
  298. {formatCurrency(
  299. state.orderHistory?.paymentMoney,
  300. state.orderHistory?.curency,
  301. )}{" "}
  302. {/* <span className="text-slate-500 font-normal">
  303. ({state.orderHistory?.curency})
  304. </span> */}
  305. </span>
  306. </div>
  307. {/* Customer Info Grid */}
  308. <div className="grid grid-cols-1 md:grid-cols-4 gap-6 md:gap-4 mt-4">
  309. <div className="flex flex-col">
  310. <div className="flex items-center gap-2 text-slate-400 mb-1">
  311. <svg
  312. className="w-4 h-4"
  313. fill="none"
  314. stroke="currentColor"
  315. viewBox="0 0 24 24"
  316. >
  317. <path
  318. strokeLinecap="round"
  319. strokeLinejoin="round"
  320. strokeWidth={2}
  321. 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"
  322. />
  323. </svg>
  324. <span className="text-xs font-bold text-[16px]">
  325. {t("fullName")}
  326. </span>
  327. </div>
  328. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  329. {state?.orderHistory.customerInfo.surName}{" "}
  330. {state?.orderHistory.customerInfo.lastName}
  331. </span>
  332. </div>
  333. <div className="flex flex-col">
  334. <div className="flex items-center gap-2 text-slate-400 mb-1">
  335. <svg
  336. className="w-4 h-4"
  337. fill="none"
  338. stroke="currentColor"
  339. viewBox="0 0 24 24"
  340. >
  341. <path
  342. strokeLinecap="round"
  343. strokeLinejoin="round"
  344. strokeWidth={2}
  345. 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"
  346. />
  347. </svg>
  348. <span className="text-xs font-bold text-[16px]">
  349. {t("phoneNumber")}
  350. </span>
  351. </div>
  352. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  353. {state?.orderHistory.customerInfo.phoneNumber}
  354. </span>
  355. </div>
  356. <div className="flex flex-col">
  357. <div className="flex items-center gap-2 text-slate-400 mb-1">
  358. <svg
  359. className="w-4 h-4"
  360. fill="none"
  361. stroke="currentColor"
  362. viewBox="0 0 24 24"
  363. >
  364. <path
  365. strokeLinecap="round"
  366. strokeLinejoin="round"
  367. strokeWidth={2}
  368. 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"
  369. />
  370. </svg>
  371. <span className="text-xs font-bold text-[16px]">Email</span>
  372. </div>
  373. <span className="text-slate-800 font-semibold text-sm text-[16px] break-all">
  374. {state?.orderHistory.customerInfo.email}
  375. </span>
  376. </div>
  377. <div className="flex flex-col">
  378. <div className="flex items-center gap-2 text-slate-400 mb-1">
  379. <svg
  380. className="w-4 h-4"
  381. fill="none"
  382. stroke="currentColor"
  383. viewBox="0 0 24 24"
  384. >
  385. <path
  386. strokeLinecap="round"
  387. strokeLinejoin="round"
  388. strokeWidth={2}
  389. 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"
  390. />
  391. </svg>
  392. <span className="text-xs font-bold text-[16px]">
  393. {t("paymentMethod")}
  394. </span>
  395. </div>
  396. <span className="text-slate-800 font-semibold text-sm text-[16px]">
  397. QR Code
  398. </span>
  399. </div>
  400. </div>
  401. </div>
  402. </div>
  403. {/* Product Section */}
  404. <div className="space-y-4">
  405. {orderDetails.length > 0 && (
  406. <div className="flex items-center space-x-3">
  407. <img
  408. src={`${orderDetails[0].areaIconUrl}`}
  409. className="w-8 h-8 rounded-full object-cover border border-slate-200"
  410. />
  411. <h3 className="text-lg font-bold text-slate-900">
  412. SIM {orderDetails[0].areaName}
  413. </h3>
  414. </div>
  415. )}
  416. <div
  417. className={`rounded-2xl border border-slate-100 shadow-sm p-6 relative ${
  418. activeTab === "manage" ? "bg-slate-50/50" : "bg-white"
  419. }`}
  420. >
  421. {activeTab === "detail" ? (
  422. // DETAIL TAB CONTENT
  423. <div className="space-y-8">
  424. {orderDetails.map((pkg, idx) => (
  425. <div
  426. key={idx}
  427. className={`flex flex-col md:flex-row justify-between items-start md:items-center ${
  428. idx !== orderDetails.length - 1
  429. ? "border-b border-slate-100 pb-8"
  430. : ""
  431. }`}
  432. >
  433. <div className="space-y-2">
  434. <div className="flex items-center gap-2">
  435. <h4 className="text-xl font-bold text-slate-800">
  436. {pkg.packageName}
  437. </h4>
  438. {/* <span className="px-2 py-0.5 rounded bg-slate-100 text-slate-600 text-xs font-bold">
  439. {pkg.id}
  440. </span> */}
  441. </div>
  442. {/* <span className="inline-block bg-[#ff0050] text-white text-[10px] font-bold px-2 py-0.5 rounded">
  443. TikTok
  444. </span> */}
  445. <p className="text-slate-500 text-sm font-bold">
  446. {t("validityPeriod")}: {pkg.dayDuration} {t("days")}
  447. </p>
  448. </div>
  449. <div className="mt-4 md:mt-0 flex flex-col items-end">
  450. <button
  451. className="flex items-center text-[#EE0434] font-bold text-sm mb-2 hover:underline"
  452. onClick={() => {
  453. dispatch(openQRModal(pkg.qrcodeUrl));
  454. }}
  455. >
  456. <svg
  457. className="w-4 h-4 mr-1"
  458. fill="none"
  459. stroke="currentColor"
  460. viewBox="0 0 24 24"
  461. >
  462. <path
  463. strokeLinecap="round"
  464. strokeLinejoin="round"
  465. strokeWidth={2}
  466. d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
  467. />
  468. <path
  469. strokeLinecap="round"
  470. strokeLinejoin="round"
  471. strokeWidth={2}
  472. 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"
  473. />
  474. </svg>
  475. QR Code
  476. </button>
  477. <div className="text-right">
  478. <span className="text-xl font-bold text-slate-800">
  479. {formatCurrency(pkg.paymentMoney, pkg.curency)}
  480. </span>
  481. {/* <span className="text-slate-500 font-medium ml-1 font-bold">
  482. {" "}
  483. đ
  484. </span> */}
  485. </div>
  486. </div>
  487. </div>
  488. ))}
  489. </div>
  490. ) : (
  491. // MANAGE TAB CONTENT - Grid of Circular Progress
  492. <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
  493. {dataUsage.map((pkg, idx) => (
  494. <div
  495. key={idx}
  496. className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm flex flex-col items-center hover:shadow-md transition-shadow"
  497. >
  498. <h4 className="text-lg font-bold text-slate-800 mb-2">
  499. {orderDetails[idx].packageName}
  500. </h4>
  501. <CircularProgress dataUsage={pkg} />
  502. </div>
  503. ))}
  504. </div>
  505. )}
  506. </div>
  507. </div>
  508. </div>
  509. </div>
  510. );
  511. };
  512. export default OrderDetailView;