ff
This commit is contained in:
Generated
+6568
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.16",
|
||||||
"@react-three/drei": "^10.7.8",
|
"@react-three/drei": "^10.7.8",
|
||||||
"@react-three/fiber": "^9.7.0",
|
"@react-three/fiber": "^9.7.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"react-redux": "^9.3.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-toastify": "^11.1.0",
|
"react-toastify": "^11.1.0",
|
||||||
"three": "^0.185.1",
|
"three": "^0.185.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||||
|
import MainPageBlueprint from "@/components/page/blueprint/MainPageBlueprint";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<BaseLayout title="Blueprint">
|
||||||
|
<MainPageBlueprint />
|
||||||
|
</BaseLayout>
|
||||||
|
// <h1>Home</h1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||||
import MainPageHome from "@/components/page/Home/MainPageHome";
|
import MainPageHome from "@/components/page/home/MainPageHome";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
<BaseLayout title="">
|
<BaseLayout title="Dashboard">
|
||||||
<MainPageHome />
|
<MainPageHome />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
// <h1>Home</h1>
|
// <h1>Home</h1>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||||
|
import MainPageUsers from "@/components/page/users/MainPageUsers";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<BaseLayout title="Quản lý người dùng">
|
||||||
|
<MainPageUsers />
|
||||||
|
</BaseLayout>
|
||||||
|
// <h1>Home</h1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
||||||
|
|
||||||
import Header from "./componets/Header";
|
import Header from "./componets/Header";
|
||||||
|
import Navbar from "./componets/Navbar";
|
||||||
import RequireAuth from "@/components/protected/RequiredAuth";
|
import RequireAuth from "@/components/protected/RequiredAuth";
|
||||||
|
|
||||||
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
||||||
@@ -12,45 +16,125 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
const [showFull, setShowFull] = React.useState(true);
|
const [showFull, setShowFull] = React.useState(true);
|
||||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
||||||
|
|
||||||
return (
|
const pathname = usePathname();
|
||||||
// <RequireAuth>
|
|
||||||
<ContextBaseLayout
|
|
||||||
value={{
|
|
||||||
showFull,
|
|
||||||
setShowFull,
|
|
||||||
openMenuMobile,
|
|
||||||
setOpenMenuMobile,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="min-h-screen w-full overflow-x-hidden bg-[#f8f8f8]">
|
|
||||||
{/* HEADER */}
|
|
||||||
<header
|
|
||||||
className={clsx(
|
|
||||||
"top-0 right-0 z-[11] h-[68px]",
|
|
||||||
"w-full",
|
|
||||||
"border-b border-[#f4f7fa]",
|
|
||||||
"bg-white",
|
|
||||||
"transition-all duration-300",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Header title={title} breadcrumb={breadcrumb} />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* MAIN CONTENT */}
|
// Blueprint layout đặc biệt
|
||||||
<main
|
const isBlueprint = pathname.startsWith("/blueprint");
|
||||||
className={clsx(
|
|
||||||
"w-full",
|
return (
|
||||||
"min-w-0",
|
<RequireAuth>
|
||||||
"overflow-x-hidden",
|
<ContextBaseLayout
|
||||||
"transition-all duration-300 ease-in-out",
|
value={{
|
||||||
"ml-0",
|
showFull,
|
||||||
|
setShowFull,
|
||||||
|
openMenuMobile,
|
||||||
|
setOpenMenuMobile,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="min-h-screen bg-[#f8f8f8]">
|
||||||
|
{/* =========================
|
||||||
|
OVERLAY MOBILE
|
||||||
|
========================= */}
|
||||||
|
{!isBlueprint && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"fixed inset-0 bg-black/50 backdrop-blur-sm z-[20]",
|
||||||
|
"transition-opacity duration-300 xl:hidden",
|
||||||
|
openMenuMobile ? "opacity-100 visible" : "opacity-0 invisible",
|
||||||
|
)}
|
||||||
|
onClick={() => setOpenMenuMobile(false)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
{children}
|
{/* =========================
|
||||||
</main>
|
SIDEBAR / NAVBAR
|
||||||
</div>
|
========================= */}
|
||||||
</ContextBaseLayout>
|
{!isBlueprint && (
|
||||||
// </RequireAuth>
|
<nav
|
||||||
|
className={clsx(
|
||||||
|
"fixed top-0 left-0 h-screen",
|
||||||
|
"bg-white border-r border-[#f4f7fa]",
|
||||||
|
"z-[30]",
|
||||||
|
"transition-all duration-300 ease-in-out",
|
||||||
|
|
||||||
|
/* DESKTOP */
|
||||||
|
showFull
|
||||||
|
? "xl:w-[240px] xl:translate-x-0"
|
||||||
|
: "xl:w-[240px] xl:-translate-x-full",
|
||||||
|
|
||||||
|
/* MOBILE + TABLET */
|
||||||
|
openMenuMobile
|
||||||
|
? "translate-x-0 w-[240px]"
|
||||||
|
: "-translate-x-full w-[240px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* =========================
|
||||||
|
HEADER
|
||||||
|
========================= */}
|
||||||
|
<header
|
||||||
|
className={clsx(
|
||||||
|
"top-0 right-0 h-[68px]",
|
||||||
|
"bg-white border-b border-[#f4f7fa]",
|
||||||
|
"z-[11]",
|
||||||
|
|
||||||
|
isBlueprint
|
||||||
|
? [
|
||||||
|
// Blueprint:
|
||||||
|
// Không fixed → scroll cùng trang
|
||||||
|
"absolute",
|
||||||
|
"left-0 w-full",
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
// Các page bình thường:
|
||||||
|
// Header cố định
|
||||||
|
"fixed",
|
||||||
|
"transition-all duration-300",
|
||||||
|
|
||||||
|
showFull
|
||||||
|
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
||||||
|
: "xl:left-0 xl:w-full",
|
||||||
|
|
||||||
|
"left-0 w-full",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Header title={title} breadcrumb={breadcrumb} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* =========================
|
||||||
|
MAIN CONTENT
|
||||||
|
========================= */}
|
||||||
|
<main
|
||||||
|
className={clsx(
|
||||||
|
"transition-all duration-300 ease-in-out",
|
||||||
|
|
||||||
|
isBlueprint
|
||||||
|
? [
|
||||||
|
// Blueprint dùng toàn bộ màn hình
|
||||||
|
"ml-0",
|
||||||
|
"px-0",
|
||||||
|
"pt-[68px]",
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
// Các page bình thường
|
||||||
|
"pt-[92px]",
|
||||||
|
"px-6",
|
||||||
|
|
||||||
|
showFull ? "xl:ml-[240px]" : "xl:ml-0",
|
||||||
|
|
||||||
|
"ml-0",
|
||||||
|
],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</ContextBaseLayout>
|
||||||
|
</RequireAuth>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -80,21 +80,21 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
{/* Nút bấm Mobile: Mở menu mobile */}
|
{/* Nút bấm Mobile: Mở menu mobile */}
|
||||||
<div
|
<div
|
||||||
className="xl:hidden cursor-pointer"
|
className="xl:hidden cursor-pointer"
|
||||||
// onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)}
|
onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)}
|
||||||
>
|
>
|
||||||
<Image src={icons.LogoTedi} alt="menu" width={20} height={20} />
|
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
||||||
</div>
|
</div>
|
||||||
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
||||||
<div
|
<div
|
||||||
className="hidden xl:block cursor-pointer"
|
className="hidden xl:block cursor-pointer"
|
||||||
// onClick={() => context?.setShowFull?.(!context?.showFull)}
|
onClick={() => context?.setShowFull?.(!context?.showFull)}
|
||||||
>
|
>
|
||||||
<Image src={icons.LogoTedi} alt="menu" width={20} height={20} />
|
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
||||||
</div>
|
</div>
|
||||||
{breadcrumb ? (
|
{breadcrumb ? (
|
||||||
breadcrumb
|
breadcrumb
|
||||||
) : (
|
) : (
|
||||||
<h4 className="text-[18px] font-bold text-[#141416]">Công ty TEDI</h4>
|
<h4 className="text-[18px] font-bold text-[#141416]">{title}</h4>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -110,7 +110,12 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
>
|
>
|
||||||
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden relative">
|
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden relative">
|
||||||
<Image
|
<Image
|
||||||
src={icons.avatar}
|
src={
|
||||||
|
// currentUserProfile?.avatarUrl
|
||||||
|
// ? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||||
|
// :
|
||||||
|
icons.avatar
|
||||||
|
}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
@@ -118,7 +123,10 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
||||||
{"User admin"}
|
{
|
||||||
|
// currentUserProfile?.fullName ||
|
||||||
|
"User admin"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { setInfoUser } from "@/redux/reducer/user";
|
|||||||
import { PATH } from "@/constant/config";
|
import { PATH } from "@/constant/config";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
import { FileBox, LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
||||||
|
|
||||||
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -51,6 +51,21 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
||||||
{/* PROFILE */}
|
{/* PROFILE */}
|
||||||
|
<Link
|
||||||
|
href={PATH.BLUEPRINT}
|
||||||
|
onClick={onClose}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 transition",
|
||||||
|
checkActive(PATH.BLUEPRINT) && "bg-gray-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FileBox size={20} />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Thiết kế cầu đường</p>
|
||||||
|
<p className="text-xs text-gray-500">Chi tiết bản vẽ</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div className="h-px bg-gray-200 my-1" />
|
||||||
|
|
||||||
{/* LOGOUT */}
|
{/* LOGOUT */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Menus, PATH } from "@/constant/config";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Hospital } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import icons from "@/constant/images/icons";
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const checkActive = useCallback(
|
||||||
|
(pathActive: string) => {
|
||||||
|
// Handle root path "/" as dashboard
|
||||||
|
if (pathname === "/" && pathActive === "/dashboard") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentRoute = pathname.split("/")[1];
|
||||||
|
return pathActive === `/${currentRoute}`;
|
||||||
|
},
|
||||||
|
[pathname],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full flex flex-col items-center p-3 bg-white">
|
||||||
|
<Link href={PATH.HOME} className="flex items-center justify-center py-2">
|
||||||
|
<Image alt="Logo" src={icons.logoIconTedi} width={40} height={40} />
|
||||||
|
{/* <Hospital className="text-blue-900" /> */}
|
||||||
|
<h2 className="ml-2 text-sm font-semibold text-blue-900 select-none">
|
||||||
|
Quản lý TEDI-DDP
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<div className="flex-1 w-full overflow-auto pt-3">
|
||||||
|
{Menus.map((menu, i) => (
|
||||||
|
<div key={i} className="mb-4">
|
||||||
|
{/* TITLE */}
|
||||||
|
<h5 className="text-xs font-semibold text-gray-400 uppercase mb-2">
|
||||||
|
{menu.title}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{menu.group.map((tab, y) => {
|
||||||
|
const isActive = checkActive(tab.pathActive);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={y}
|
||||||
|
href={tab.path}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center gap-2 px-3 py-2 rounded-lg transition",
|
||||||
|
isActive
|
||||||
|
? "bg-indigo-100 text-blue-700"
|
||||||
|
: "text-gray-800 hover:bg-indigo-100 hover:text-blue-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<tab.icon
|
||||||
|
size={22}
|
||||||
|
className={clsx(
|
||||||
|
isActive ? "text-blue-700" : "text-gray-800",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-base font-semibold">{tab.title}</p>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./Navbar";
|
||||||
@@ -1,27 +1,7 @@
|
|||||||
"use client";
|
import React from "react";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
const MainPageHome = () => {
|
||||||
import BeamForm from "./components/BeamForm";
|
return <div>MainPageHome</div>;
|
||||||
import { BridgeTreeSidebar } from "@/components/utils/BridgeTreeSidebar";
|
};
|
||||||
|
|
||||||
const Canvas3D = dynamic(
|
export default MainPageHome;
|
||||||
() => import("@/components/viewer3d/Canvas3D").then((m) => m.Canvas3D),
|
|
||||||
{ ssr: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<main className="w-screen h-screen flex overflow-hidden bg-slate-950">
|
|
||||||
{/* 1. Cột Menu cây thư mục (Trái) */}
|
|
||||||
<BridgeTreeSidebar />
|
|
||||||
|
|
||||||
{/* 2. Cột Form nhập liệu theo Tab (Giữa) */}
|
|
||||||
<BeamForm />
|
|
||||||
|
|
||||||
{/* 3. Cột Viewport 3D Focus theo Cấu kiện (Phải) */}
|
|
||||||
<div className="flex-1 h-full relative">
|
|
||||||
<Canvas3D />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
|
||||||
import {
|
|
||||||
Download,
|
|
||||||
Upload,
|
|
||||||
AlertTriangle,
|
|
||||||
CheckCircle2,
|
|
||||||
Layers,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
|
||||||
|
|
||||||
export function BeamForm() {
|
|
||||||
const {
|
|
||||||
activeTab,
|
|
||||||
globalConfig,
|
|
||||||
setGlobalConfig,
|
|
||||||
beamData,
|
|
||||||
setBeamData,
|
|
||||||
bridgeData,
|
|
||||||
setBridgeData,
|
|
||||||
isAssemblyMode,
|
|
||||||
setIsAssemblyMode,
|
|
||||||
validationIssues,
|
|
||||||
exportJSONContract,
|
|
||||||
importJSONContract,
|
|
||||||
} = useSuperTStore();
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleExportContract = () => {
|
|
||||||
const jsonString = exportJSONContract();
|
|
||||||
const blob = new Blob([jsonString], { type: "application/json" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.download = `TEDI_SuperT_Contract_${Date.now()}.json`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
const content = event.target?.result as string;
|
|
||||||
if (content) {
|
|
||||||
const success = importJSONContract(content);
|
|
||||||
if (success) {
|
|
||||||
alert("Đã Regenerate thành công mô hình từ file JSON Contract!");
|
|
||||||
} else {
|
|
||||||
alert("File cấu hình không hợp lệ!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-[360px] h-full bg-slate-900 border-r border-slate-800 text-slate-100 p-4 overflow-y-auto flex flex-col gap-5 shrink-0 select-none">
|
|
||||||
{/* Nút bật/tắt Chế độ Ghép Toàn Cầu */}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsAssemblyMode(!isAssemblyMode)}
|
|
||||||
className={`w-full py-2 px-3 rounded text-xs font-semibold flex items-center justify-center gap-2 transition-all border ${
|
|
||||||
isAssemblyMode
|
|
||||||
? "bg-emerald-600 hover:bg-emerald-500 border-emerald-400 text-white shadow-lg shadow-emerald-600/20"
|
|
||||||
: "bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Layers className="w-4 h-4 shrink-0" />
|
|
||||||
<span>
|
|
||||||
{isAssemblyMode
|
|
||||||
? "Chế độ: Mô hình tổng thể Cầu"
|
|
||||||
: "Xem riêng lẻ cấu kiện"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex-1 space-y-4">
|
|
||||||
{/* TAB CẤU HÌNH TỔNG THỂ CẦU (ALIGNMENT) */}
|
|
||||||
{activeTab === "alignment" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
|
||||||
Cấu hình Nhịp & Tuyến Cầu
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
|
||||||
Số lượng nhịp cầu (N ≥ 2):
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={2}
|
|
||||||
max={10}
|
|
||||||
value={globalConfig.spanCount}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGlobalConfig({
|
|
||||||
spanCount: Math.max(2, Number(e.target.value)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
|
||||||
Số dầm Super-T / nhịp:
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={2}
|
|
||||||
max={8}
|
|
||||||
value={globalConfig.beamCountPerSpan}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGlobalConfig({ beamCountPerSpan: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
|
||||||
Khoảng cách giữa các dầm (mm):
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={globalConfig.beamSpacing}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGlobalConfig({ beamSpacing: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* TAB DẦM SUPER-T */}
|
|
||||||
{activeTab === "superTBeam" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
|
||||||
1.1 Thông số Dầm Super-T
|
|
||||||
</h2>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
|
||||||
Chiều dài L (mm):
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={beamData.length}
|
|
||||||
onChange={(e) =>
|
|
||||||
setBeamData({ length: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
|
||||||
Chiều cao H (mm):
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={beamData.height}
|
|
||||||
onChange={(e) =>
|
|
||||||
setBeamData({ height: Number(e.target.value) })
|
|
||||||
}
|
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Panel Cảnh báo Validation */}
|
|
||||||
<div className="pt-2">
|
|
||||||
{validationIssues && validationIssues.length > 0 ? (
|
|
||||||
<div className="p-2.5 bg-rose-500/10 border border-rose-500/30 rounded text-rose-400 text-xs space-y-1">
|
|
||||||
<div className="flex items-center gap-1.5 font-semibold">
|
|
||||||
<AlertTriangle className="w-3.5 h-3.5" />
|
|
||||||
<span>Cảnh báo kỹ thuật:</span>
|
|
||||||
</div>
|
|
||||||
<ul className="list-disc list-inside text-[11px] space-y-0.5 text-rose-300">
|
|
||||||
{validationIssues.map((issue, idx) => (
|
|
||||||
<li key={idx}>{issue.message}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="p-2 bg-emerald-500/10 border border-emerald-500/30 rounded text-emerald-400 text-xs flex items-center gap-1.5">
|
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
|
||||||
<span>Kích thước dầm hợp lệ (AASHTO / TCVN)</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* BỔ SUNG THÊM CÁC TAB BẢN MẶT CẦU, TRỤ, MỐ, CỌC TƯƠNG TỰ... */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cụm Nút Xuất/Nhập Data Contract & Regenerate */}
|
|
||||||
<div className="mt-auto space-y-2">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleImportFile}
|
|
||||||
accept=".json"
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
className="w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium py-2 px-3 rounded text-xs flex items-center justify-center gap-2 border border-slate-700 transition-all"
|
|
||||||
>
|
|
||||||
<Upload className="w-3.5 h-3.5" />
|
|
||||||
<span>Import Config (Regenerate)</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleExportContract}
|
|
||||||
className="w-full bg-sky-600 hover:bg-sky-500 active:bg-sky-700 text-white font-medium py-2.5 px-3 rounded text-xs flex items-center justify-center gap-2 transition-all shadow-lg shadow-sky-600/20"
|
|
||||||
>
|
|
||||||
<Download className="w-4 h-4 shrink-0" />
|
|
||||||
<span>Xuất Data Contract (JSON)</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BeamForm;
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import BeamForm from "./components/BeamForm";
|
||||||
|
import { BridgeTreeSidebar } from "@/components/utils/BridgeTreeSidebar";
|
||||||
|
const Canvas3D = dynamic(
|
||||||
|
() => import("@/components/viewer3d/Canvas3D").then((m) => m.Canvas3D),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
export default function MainPageBlueprint() {
|
||||||
|
return (
|
||||||
|
<main className="w-screen h-screen flex overflow-hidden bg-slate-950">
|
||||||
|
{/* 1. Cột Menu cây thư mục (Trái) */}
|
||||||
|
<BridgeTreeSidebar />
|
||||||
|
{/* 2. Cột Form nhập liệu theo Tab (Giữa) */}
|
||||||
|
<BeamForm />
|
||||||
|
{/* 3. Cột Viewport 3D Focus theo Cấu kiện (Phải) */}
|
||||||
|
<div className="flex-1 h-full relative">
|
||||||
|
<Canvas3D />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,630 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
Layers,
|
||||||
|
Box,
|
||||||
|
Building2,
|
||||||
|
Route,
|
||||||
|
Eye,
|
||||||
|
FileSpreadsheet,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
||||||
|
import { exportToExcelContract } from "@/components/utils/excelExporter";
|
||||||
|
|
||||||
|
export function BeamForm() {
|
||||||
|
const {
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
globalConfig,
|
||||||
|
setGlobalConfig,
|
||||||
|
beamData,
|
||||||
|
setBeamData,
|
||||||
|
bridgeData,
|
||||||
|
setBridgeData,
|
||||||
|
isAssemblyMode,
|
||||||
|
setIsAssemblyMode,
|
||||||
|
validationIssues,
|
||||||
|
exportJSONContract,
|
||||||
|
importJSONContract,
|
||||||
|
} = useSuperTStore();
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Xuất file JSON Contract
|
||||||
|
const handleExportContract = () => {
|
||||||
|
const jsonString = exportJSONContract();
|
||||||
|
const blob = new Blob([jsonString], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = `TEDI_SuperT_FullContract_${Date.now()}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Xuất file Excel Contract chuẩn Revit Add-in (S8.5 & S8.6)
|
||||||
|
const handleExportExcel = () => {
|
||||||
|
try {
|
||||||
|
exportToExcelContract(beamData, globalConfig, bridgeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Lỗi xuất Excel:", error);
|
||||||
|
alert("Có lỗi xảy ra khi xuất file Excel!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
if (extension === "json") {
|
||||||
|
const content = await file.text();
|
||||||
|
const success = importJSONContract(content);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
alert("File JSON không hợp lệ hoặc thiếu payload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Import JSON Contract thành công!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extension === "xlsx" || extension === "xls") {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
const workbook = XLSX.read(buffer, {
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheetNames = workbook.SheetNames;
|
||||||
|
|
||||||
|
if (sheetNames.length === 0) {
|
||||||
|
alert("File Excel không có worksheet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra tên các sheet trước khi ánh xạ dữ liệu.
|
||||||
|
console.log("Các worksheet trong Excel:", sheetNames);
|
||||||
|
|
||||||
|
const workbookData = Object.fromEntries(
|
||||||
|
sheetNames.map((sheetName) => [
|
||||||
|
sheetName,
|
||||||
|
XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], {
|
||||||
|
defval: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Dữ liệu Excel:", workbookData);
|
||||||
|
|
||||||
|
alert(
|
||||||
|
"Đã đọc được file Excel. Cần ánh xạ các worksheet và cột Excel " +
|
||||||
|
"sang cấu trúc GlobalBridgeConfig, beamData và bridgeData trước khi cập nhật mô hình.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(
|
||||||
|
"Định dạng không được hỗ trợ. Vui lòng chọn file .json hoặc .xlsx.",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Lỗi import file:", error);
|
||||||
|
alert(
|
||||||
|
"Không thể đọc file. Vui lòng kiểm tra định dạng và nội dung file.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Cho phép chọn lại cùng một file.
|
||||||
|
e.target.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[380px] h-full bg-slate-900 border-r border-slate-800 text-slate-100 p-4 overflow-y-auto flex flex-col gap-4 shrink-0 select-none text-xs">
|
||||||
|
{/* Control Chế độ xem */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsAssemblyMode(!isAssemblyMode)}
|
||||||
|
className={`w-full py-2 px-3 rounded font-semibold flex items-center justify-center gap-2 transition-all border ${
|
||||||
|
isAssemblyMode
|
||||||
|
? "bg-emerald-600 hover:bg-emerald-500 border-emerald-400 text-white shadow-lg shadow-emerald-600/20"
|
||||||
|
: "bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Layers className="w-4 h-4 shrink-0" />
|
||||||
|
<span>
|
||||||
|
{isAssemblyMode
|
||||||
|
? "Chế độ: Mô hình Tổng thể"
|
||||||
|
: "Xem Riêng lẻ Cấu kiện"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Thanh chọn Tab Phân loại Cấu kiện (ClassCode Grouping) */}
|
||||||
|
<div className="grid grid-cols-4 gap-1 bg-slate-950 p-1 rounded border border-slate-800">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("alignment")}
|
||||||
|
className={`py-1.5 px-1 rounded flex flex-col items-center gap-1 ${
|
||||||
|
activeTab === "alignment"
|
||||||
|
? "bg-sky-600 text-white"
|
||||||
|
: "text-slate-400 hover:text-slate-200"
|
||||||
|
}`}
|
||||||
|
title="Tuyến & Nhịp"
|
||||||
|
>
|
||||||
|
<Route className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-[10px]">Tuyến</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("superTBeam")}
|
||||||
|
className={`py-1.5 px-1 rounded flex flex-col items-center gap-1 ${
|
||||||
|
activeTab === "superTBeam" ||
|
||||||
|
activeTab === "deck" ||
|
||||||
|
activeTab === "diaphragm" ||
|
||||||
|
activeTab === "railing"
|
||||||
|
? "bg-sky-600 text-white"
|
||||||
|
: "text-slate-400 hover:text-slate-200"
|
||||||
|
}`}
|
||||||
|
title="Kết cấu thượng tầng (Superstructure)"
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-[10px]">Thượng tầng</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("pier")}
|
||||||
|
className={`py-1.5 px-1 rounded flex flex-col items-center gap-1 ${
|
||||||
|
activeTab === "pier" ||
|
||||||
|
activeTab === "abutment" ||
|
||||||
|
activeTab === "pile" ||
|
||||||
|
activeTab === "bearing"
|
||||||
|
? "bg-sky-600 text-white"
|
||||||
|
: "text-slate-400 hover:text-slate-200"
|
||||||
|
}`}
|
||||||
|
title="Kết cấu hạ tầng (Substructure)"
|
||||||
|
>
|
||||||
|
<Building2 className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-[10px]">Hạ tầng</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("beamDeckJoint")}
|
||||||
|
className={`py-1.5 px-1 rounded flex flex-col items-center gap-1 ${
|
||||||
|
activeTab === "beamDeckJoint"
|
||||||
|
? "bg-sky-600 text-white"
|
||||||
|
: "text-slate-400 hover:text-slate-200"
|
||||||
|
}`}
|
||||||
|
title="Phạm vi Đường dẫn & Khác"
|
||||||
|
>
|
||||||
|
<Eye className="w-3.5 h-3.5" />
|
||||||
|
<span className="text-[10px]">Đường dẫn</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
{/* ALIGNMENT / TUYẾN CẦU */}
|
||||||
|
{activeTab === "alignment" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h2 className="font-bold text-sky-400 uppercase tracking-wider text-[11px]">
|
||||||
|
Cấu hình Nhịp & Tuyến Cầu
|
||||||
|
</h2>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Số lượng nhịp cầu (N ≥ 2):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={2}
|
||||||
|
max={10}
|
||||||
|
value={globalConfig.spanCount}
|
||||||
|
onChange={(e) =>
|
||||||
|
setGlobalConfig({
|
||||||
|
spanCount: Math.max(2, Number(e.target.value)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Số dầm Super-T / nhịp:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={2}
|
||||||
|
max={8}
|
||||||
|
value={globalConfig.beamCountPerSpan}
|
||||||
|
onChange={(e) =>
|
||||||
|
setGlobalConfig({ beamCountPerSpan: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Khoảng cách giữa các dầm (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={globalConfig.beamSpacing}
|
||||||
|
onChange={(e) =>
|
||||||
|
setGlobalConfig({ beamSpacing: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* NHÓM 1: SUPERSTRUCTURE (KẾT CẤU THƯỢNG TẦNG) */}
|
||||||
|
{(activeTab === "superTBeam" ||
|
||||||
|
activeTab === "deck" ||
|
||||||
|
activeTab === "diaphragm" ||
|
||||||
|
activeTab === "railing") && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex border-b border-slate-800 pb-2 gap-2 text-[11px]">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("superTBeam")}
|
||||||
|
className={`px-2 py-0.5 rounded ${activeTab === "superTBeam" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Dầm Super-T
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("deck")}
|
||||||
|
className={`px-2 py-0.5 rounded ${activeTab === "deck" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Bản Mặt Cầu
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("diaphragm")}
|
||||||
|
className={`px-2 py-0.5 rounded ${activeTab === "diaphragm" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Dầm Ngang
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === "superTBeam" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-40.10 | BR-INC-002
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều dài L (m):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={beamData.length}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBeamData({ length: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều cao H (m):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={beamData.height}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBeamData({ height: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "deck" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-40.30 | BR-INC-001 / BR-INC-009
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Bề rộng bản mặt cầu (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.deck.width}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("deck", { width: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều dày bản mặt cầu (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.deck.thickness}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("deck", {
|
||||||
|
thickness: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "diaphragm" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-40.20 | BR-INC-003 (Vách/Dầm ngang)
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều dày dầm ngang (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.diaphragm.thickness}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("diaphragm", {
|
||||||
|
thickness: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Số lượng dầm ngang/nhịp:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.diaphragm.count}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("diaphragm", {
|
||||||
|
count: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* NHÓM 2: SUBSTRUCTURE (KẾT CẤU HẠ TẦNG) */}
|
||||||
|
{(activeTab === "pier" ||
|
||||||
|
activeTab === "abutment" ||
|
||||||
|
activeTab === "pile" ||
|
||||||
|
activeTab === "bearing") && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex border-b border-slate-800 pb-2 gap-2 text-[11px] overflow-x-auto">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("pier")}
|
||||||
|
className={`px-2 py-0.5 rounded whitespace-nowrap ${activeTab === "pier" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Trụ Cầu
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("abutment")}
|
||||||
|
className={`px-2 py-0.5 rounded whitespace-nowrap ${activeTab === "abutment" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Mố Cầu
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("bearing")}
|
||||||
|
className={`px-2 py-0.5 rounded whitespace-nowrap ${activeTab === "bearing" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Gối Cầu
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("pile")}
|
||||||
|
className={`px-2 py-0.5 rounded whitespace-nowrap ${activeTab === "pile" ? "bg-slate-800 text-sky-400 font-semibold" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
|
Cọc & Bệ Cọc
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === "pier" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-30.10 | BR-INC-018 (Thân trụ & Xà mũ)
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều cao trụ (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.pier.height}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("pier", { height: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Đường kính cột trụ (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.pier.diameter}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("pier", {
|
||||||
|
diameter: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "abutment" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-20 / BR-20.20 | BR-INC-013 / BR-INC-017
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều cao mố (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.abutment.height}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("abutment", {
|
||||||
|
height: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Bề rộng đỉnh mố (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.abutment.topWidth}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("abutment", {
|
||||||
|
topWidth: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "bearing" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-50.10 | BR-INC-019 (Gối & Bản đệm)
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều cao gối (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.bearing.height}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("bearing", {
|
||||||
|
height: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "pile" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<span className="text-[10px] text-slate-500 block">
|
||||||
|
ClassCode: BR-10 | BR-INC-015 (Cọc & Bệ cọc)
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Đường kính cọc (mm):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.pile.diameter}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("pile", {
|
||||||
|
diameter: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-400 block mb-1">
|
||||||
|
Chiều dài cọc (m):
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={bridgeData.pile.length}
|
||||||
|
onChange={(e) =>
|
||||||
|
setBridgeData("pile", { length: Number(e.target.value) })
|
||||||
|
}
|
||||||
|
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-slate-200 focus:border-sky-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validation Errors Display */}
|
||||||
|
{validationIssues && validationIssues.length > 0 ? (
|
||||||
|
<div className="p-2.5 bg-rose-500/10 border border-rose-500/30 rounded text-rose-400 space-y-1">
|
||||||
|
<div className="flex items-center gap-1.5 font-semibold">
|
||||||
|
<AlertTriangle className="w-3.5 h-3.5" />
|
||||||
|
<span>Cảnh báo kỹ thuật:</span>
|
||||||
|
</div>
|
||||||
|
<ul className="list-disc list-inside text-[11px] space-y-0.5 text-rose-300">
|
||||||
|
{validationIssues.map((issue, idx) => (
|
||||||
|
<li key={idx}>{issue.message}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-2 bg-emerald-500/10 border border-emerald-500/30 rounded text-emerald-400 flex items-center gap-1.5">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||||
|
<span>Thông số phù hợp quy chuẩn BIM/EIR</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tải/Xuất Data Contract (JSON & Excel) */}
|
||||||
|
<div className="mt-auto space-y-2 pt-2 border-t border-slate-800">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleImportFile}
|
||||||
|
accept=".xlsx,.json"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium py-2 px-3 rounded flex items-center justify-center gap-2 border border-slate-700 transition-all"
|
||||||
|
>
|
||||||
|
<Upload className="w-3.5 h-3.5" />
|
||||||
|
<span>Import Config (Regenerate)</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Nút Xuất JSON Contract */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportContract}
|
||||||
|
className="w-full bg-sky-600 hover:bg-sky-500 active:bg-sky-700 text-white font-medium py-2 px-3 rounded flex items-center justify-center gap-2 transition-all shadow-lg shadow-sky-600/20"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 shrink-0" />
|
||||||
|
<span>Xuất Data Contract (JSON)</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Nút Xuất Excel Contract (Đáp ứng yêu cầu S8.5) */}
|
||||||
|
<button
|
||||||
|
onClick={handleExportExcel}
|
||||||
|
className="w-full bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 text-white font-medium py-2 px-3 rounded flex items-center justify-center gap-2 transition-all shadow-lg shadow-emerald-600/20"
|
||||||
|
>
|
||||||
|
<FileSpreadsheet className="w-4 h-4 shrink-0" />
|
||||||
|
<span>Xuất Excel Contract (Revit)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BeamForm;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./MainPageBlueprint";
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { Users } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@radix-ui/react-tooltip";
|
||||||
|
|
||||||
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
import Search from "@/components/customs/custom-search";
|
||||||
|
import Table from "@/components/customs/custom-table";
|
||||||
|
import DataWrapper from "@/components/customs/DataWrapper";
|
||||||
|
|
||||||
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
|
import { httpRequest } from "@/services";
|
||||||
|
import usersServices, {
|
||||||
|
UsersItems,
|
||||||
|
UsersResponse,
|
||||||
|
} from "@/services/usersServices";
|
||||||
|
|
||||||
|
const MainPageUsers = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// QUERY PARAMS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const _status = searchParams.get("_status") ?? "";
|
||||||
|
const _roleId = searchParams.get("_roleId") ?? "";
|
||||||
|
const _page = Number(searchParams.get("_page") ?? 1);
|
||||||
|
const _pageSize = Number(searchParams.get("_pageSize") ?? 10);
|
||||||
|
const _keyword = searchParams.get("_keyword") ?? "";
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// GET USERS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const usersQuery = useQuery<UsersResponse>({
|
||||||
|
queryKey: [
|
||||||
|
QUERY_KEY.table_list_users,
|
||||||
|
_page,
|
||||||
|
_pageSize,
|
||||||
|
_keyword,
|
||||||
|
_status,
|
||||||
|
_roleId,
|
||||||
|
],
|
||||||
|
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await httpRequest<UsersResponse>({
|
||||||
|
showMessageFailed: true,
|
||||||
|
|
||||||
|
http: usersServices.getUsers({
|
||||||
|
page: _page,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
search: _keyword,
|
||||||
|
status: _status,
|
||||||
|
roleId: _roleId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
res ?? {
|
||||||
|
items: [],
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// UPDATE QUERY PARAMS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const updateQuery = (key: string, value?: string | number | null) => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
|
const stringValue =
|
||||||
|
value !== undefined && value !== null ? String(value).trim() : "";
|
||||||
|
|
||||||
|
if (!stringValue) {
|
||||||
|
params.delete(key);
|
||||||
|
} else {
|
||||||
|
params.set(key, stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Khi thay đổi search/filter thì quay về page 1
|
||||||
|
if (key === "_keyword" || key === "_status" || key === "_roleId") {
|
||||||
|
params.set("_page", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = params.toString();
|
||||||
|
|
||||||
|
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// TABLE COLUMNS
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
fixedLeft: true,
|
||||||
|
title: "Tên người dùng",
|
||||||
|
|
||||||
|
render: (item: UsersItems) => (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Link
|
||||||
|
href={`/users/${item.userId}`}
|
||||||
|
className="text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{item.username}
|
||||||
|
</Link>
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={6}
|
||||||
|
className="rounded-md bg-gray-800 px-3 py-2 text-sm text-white shadow-lg"
|
||||||
|
>
|
||||||
|
Chi tiết người dùng
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// RENDER
|
||||||
|
// =========================================================
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||||
|
{/* =====================================================
|
||||||
|
HEADER
|
||||||
|
===================================================== */}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h2 className="text-xl font-semibold text-black">Quản lý người dùng</h2>
|
||||||
|
|
||||||
|
<CustomButton
|
||||||
|
variant="midnightBlue"
|
||||||
|
fullWidth={false}
|
||||||
|
icon={<Users size={18} />}
|
||||||
|
>
|
||||||
|
Thêm người dùng
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* =====================================================
|
||||||
|
SEARCH
|
||||||
|
===================================================== */}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="w-full md:min-w-[400px]">
|
||||||
|
<Search
|
||||||
|
keyword={_keyword}
|
||||||
|
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||||
|
placeholder="Tìm kiếm theo tên người dùng, ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* =====================================================
|
||||||
|
TABLE
|
||||||
|
===================================================== */}
|
||||||
|
|
||||||
|
<DataWrapper
|
||||||
|
data={usersQuery.data}
|
||||||
|
loading={usersQuery.isLoading}
|
||||||
|
title="Không có người dùng"
|
||||||
|
note="Hiện tại chưa có dữ liệu người dùng."
|
||||||
|
>
|
||||||
|
<TooltipProvider delayDuration={300}>
|
||||||
|
<Table
|
||||||
|
rowKey={(item) => item.userId}
|
||||||
|
data={usersQuery.data?.items ?? []}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</DataWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainPageUsers;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./MainPageUsers";
|
||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Building2,
|
Building2,
|
||||||
MapPin,
|
MapPin,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { routerServerGlobal } from "next/dist/server/lib/router-utils/router-server-context";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface MenuNode {
|
interface MenuNode {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -60,6 +62,7 @@ const bridgeTreeData: MenuNode[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const BridgeTreeSidebar: React.FC = () => {
|
export const BridgeTreeSidebar: React.FC = () => {
|
||||||
|
const router = useRouter();
|
||||||
const { activeTab, setActiveTab } = useSuperTStore();
|
const { activeTab, setActiveTab } = useSuperTStore();
|
||||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({
|
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({
|
||||||
superstructure: true,
|
superstructure: true,
|
||||||
@@ -73,7 +76,9 @@ export const BridgeTreeSidebar: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-64 h-full bg-slate-900 border-r border-slate-700 p-3 text-white overflow-y-auto shrink-0 select-none">
|
<div className="w-64 h-full bg-slate-900 border-r border-slate-700 p-3 text-white overflow-y-auto shrink-0 select-none">
|
||||||
<div className="mb-4 px-2">
|
<div className="mb-4 px-2">
|
||||||
<h1 className="text-base font-bold text-sky-400">TEDI DDP - Super-T</h1>
|
<h1 className="text-base font-bold text-sky-400">
|
||||||
|
<span onClick={() => router.back()}>←</span> TEDI DDP - Super-T
|
||||||
|
</h1>
|
||||||
<p className="text-[10px] text-slate-400">Mô hình cấu kiện Cầu KTS</p>
|
<p className="text-[10px] text-slate-400">Mô hình cấu kiện Cầu KTS</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import * as XLSX from "xlsx";
|
||||||
|
import { SuperTBeamData } from "../core/schema/superT.schema";
|
||||||
|
|
||||||
|
export function exportToExcelContract(
|
||||||
|
beamData: SuperTBeamData,
|
||||||
|
globalConfig: any,
|
||||||
|
bridgeData: any,
|
||||||
|
) {
|
||||||
|
// 1. Tạo Workbook mới
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// 2. Sheet 1: Thông số Dầm Super-T
|
||||||
|
const beamSheetData = [
|
||||||
|
["Tham số (Parameter)", "Giá trị (Value)", "Đơn vị (Unit)", "Mô tả"],
|
||||||
|
["ID", beamData.id, "", "Mã định danh dầm"],
|
||||||
|
["Code", beamData.code, "", "Mã hiệu dầm"],
|
||||||
|
["Length", beamData.length, "m", "Chiều dài dầm"],
|
||||||
|
["Height", beamData.height, "m", "Chiều cao dầm"],
|
||||||
|
["TopFlangeWidth", beamData.topFlangeWidth, "m", "Chiều rộng cánh trên"],
|
||||||
|
[
|
||||||
|
"TopFlangeThickness",
|
||||||
|
beamData.topFlangeThickness,
|
||||||
|
"m",
|
||||||
|
"Chiều dày cánh trên",
|
||||||
|
],
|
||||||
|
["WebThickness", beamData.webThickness, "m", "Chiều dày sườn dầm"],
|
||||||
|
[
|
||||||
|
"BottomFlangeWidth",
|
||||||
|
beamData.bottomFlangeWidth,
|
||||||
|
"m",
|
||||||
|
"Chiều rộng cánh dưới",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"BottomFlangeThickness",
|
||||||
|
beamData.bottomFlangeThickness,
|
||||||
|
"m",
|
||||||
|
"Chiều dày cánh dưới",
|
||||||
|
],
|
||||||
|
["StrandCount", beamData.strandCount, "sợi", "Số lượng tao cáp DƯL"],
|
||||||
|
[
|
||||||
|
"DiaphragmPositions",
|
||||||
|
beamData.diaphragmPositions.join("; "),
|
||||||
|
"m",
|
||||||
|
"Vị trí dầm ngang",
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const beamSheet = XLSX.utils.aoa_to_sheet(beamSheetData);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, beamSheet, "SuperT_Beam");
|
||||||
|
|
||||||
|
// 3. Sheet 2: Cấu hình Chung & Tổng thể Cầu (Alignment & Assembly)
|
||||||
|
const bridgeSheetData = [
|
||||||
|
["Tham số (Parameter)", "Giá trị (Value)", "Đơn vị"],
|
||||||
|
["SpanCount", globalConfig.spanCount, "nhịp"],
|
||||||
|
["BeamCountPerSpan", globalConfig.beamCountPerSpan, "dầm/nhịp"],
|
||||||
|
["BeamSpacing", globalConfig.beamSpacing, "mm"],
|
||||||
|
["DeckWidth", bridgeData.deck.width, "mm"],
|
||||||
|
["DeckThickness", bridgeData.deck.thickness, "mm"],
|
||||||
|
["PierHeight", bridgeData.pier.height, "mm"],
|
||||||
|
["PierDiameter", bridgeData.pier.diameter, "mm"],
|
||||||
|
];
|
||||||
|
const bridgeSheet = XLSX.utils.aoa_to_sheet(bridgeSheetData);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, bridgeSheet, "Bridge_Assembly");
|
||||||
|
|
||||||
|
// 4. Tải file Excel về client
|
||||||
|
XLSX.writeFile(workbook, `TEDI_SuperT_Contract_${Date.now()}.xlsx`);
|
||||||
|
}
|
||||||
@@ -35,3 +35,64 @@ export const SuperTBeamMesh: React.FC<{ data: any }> = ({ data }) => {
|
|||||||
</mesh>
|
</mesh>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// "use client";
|
||||||
|
|
||||||
|
// import React, { useMemo } from "react";
|
||||||
|
// import * as THREE from "three";
|
||||||
|
// import { SuperTBeamData } from "../core/schema/superT.schema";
|
||||||
|
|
||||||
|
// export const SuperTBeamMesh: React.FC<{ data: SuperTBeamData }> = ({
|
||||||
|
// data,
|
||||||
|
// }) => {
|
||||||
|
// const beamShape = useMemo(() => {
|
||||||
|
// const shape = new THREE.Shape();
|
||||||
|
|
||||||
|
// // Chuyển đổi đơn vị từ mét sang mm nếu cần
|
||||||
|
// const topW = (data.topFlangeWidth || 2.1) * 1000;
|
||||||
|
// const topT = (data.topFlangeThickness || 0.2) * 1000;
|
||||||
|
// const H = (data.height || 1.75) * 1000;
|
||||||
|
// const webT = (data.webThickness || 0.18) * 1000;
|
||||||
|
// const botW = (data.bottomFlangeWidth || 0.8) * 1000;
|
||||||
|
// const botT = (data.bottomFlangeThickness || 0.3) * 1000;
|
||||||
|
|
||||||
|
// const halfTop = topW / 2;
|
||||||
|
// const halfBot = botW / 2;
|
||||||
|
// const halfWeb = webT / 2;
|
||||||
|
|
||||||
|
// // Vẽ nửa bên phải tiết diện chữ T/I ngược dầm Super-T
|
||||||
|
// shape.moveTo(0, 0);
|
||||||
|
// shape.lineTo(halfBot, 0);
|
||||||
|
// shape.lineTo(halfBot, botT);
|
||||||
|
// shape.lineTo(halfWeb, botT + 150);
|
||||||
|
// shape.lineTo(halfWeb, H - topT);
|
||||||
|
// shape.lineTo(halfTop, H - topT);
|
||||||
|
// shape.lineTo(halfTop, H);
|
||||||
|
// shape.lineTo(-halfTop, H);
|
||||||
|
// shape.lineTo(-halfTop, H - topT);
|
||||||
|
// shape.lineTo(-halfWeb, H - topT);
|
||||||
|
// shape.lineTo(-halfWeb, botT + 150);
|
||||||
|
// shape.lineTo(-halfBot, botT);
|
||||||
|
// shape.lineTo(-halfBot, 0);
|
||||||
|
// shape.closePath();
|
||||||
|
|
||||||
|
// return shape;
|
||||||
|
// }, [data]);
|
||||||
|
|
||||||
|
// const lengthMM = (data.length || 33) * 1000;
|
||||||
|
|
||||||
|
// const extrudeSettings = useMemo(
|
||||||
|
// () => ({
|
||||||
|
// depth: lengthMM,
|
||||||
|
// bevelEnabled: false,
|
||||||
|
// }),
|
||||||
|
// [lengthMM],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <mesh position={[0, 0, -lengthMM / 2]}>
|
||||||
|
// <extrudeGeometry args={[beamShape, extrudeSettings]} />
|
||||||
|
// <meshStandardMaterial color="#38bdf8" roughness={0.4} metalness={0.1} />
|
||||||
|
// </mesh>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|||||||
@@ -1,34 +1,19 @@
|
|||||||
export enum QUERY_KEY {
|
export enum QUERY_KEY {
|
||||||
table_list_patient,
|
table_list_users,
|
||||||
table_list_consultation,
|
|
||||||
table_list_patientrescription,
|
|
||||||
table_list_specialty,
|
|
||||||
table_list_report,
|
|
||||||
table_list_profile,
|
|
||||||
|
|
||||||
list_specialty_lookup,
|
|
||||||
examination_specialty_list,
|
|
||||||
|
|
||||||
chi_tiet_benh_nhan,
|
|
||||||
chi_tiet_don_thuoc,
|
|
||||||
chi_tiet_phien_kham,
|
|
||||||
chi_tiet_thong_tin,
|
|
||||||
|
|
||||||
user_me,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draft = 1,
|
// Draft = 1,
|
||||||
// InProgress = 2,
|
// InProgress = 2,
|
||||||
// Completed = 3,
|
// Completed = 3,
|
||||||
// Cancelled = 4,
|
// Cancelled = 4,
|
||||||
export enum TYPE_STATUS {
|
// export enum TYPE_STATUS {
|
||||||
Draft = 1, // Phiên tạm (mới tiếp nhận, chưa khám)
|
// Draft = 1, // Phiên tạm (mới tiếp nhận, chưa khám)
|
||||||
InProgress = 2, // Đang khám tổng quát
|
// InProgress = 2, // Đang khám tổng quát
|
||||||
ToSpecialty = 3, // Đã chỉ định & Đang chờ khám chuyên khoa 💡 (MỚI)
|
// ToSpecialty = 3, // Đã chỉ định & Đang chờ khám chuyên khoa 💡 (MỚI)
|
||||||
PendingPrescription = 4, // Đã khám xong hết, Chờ quầy dược phát thuốc/kính 💡 (MỚI)
|
// PendingPrescription = 4, // Đã khám xong hết, Chờ quầy dược phát thuốc/kính 💡 (MỚI)
|
||||||
Completed = 5, // Đã hoàn thành hoàn toàn (Đã khám + Đã nhận thuốc)
|
// Completed = 5, // Đã hoàn thành hoàn toàn (Đã khám + Đã nhận thuốc)
|
||||||
Cancelled = 6, // Phiên khám bị hủy
|
// Cancelled = 6, // Phiên khám bị hủy
|
||||||
}
|
// }
|
||||||
|
|
||||||
export enum TYPE_GENDER {
|
export enum TYPE_GENDER {
|
||||||
MALE = "Nam",
|
MALE = "Nam",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// import {
|
import {
|
||||||
// ChartNoAxesCombined,
|
ChartNoAxesCombined,
|
||||||
// CircleGauge,
|
CircleGauge,
|
||||||
// HeartPulse,
|
HeartPulse,
|
||||||
// Pill,
|
Pill,
|
||||||
// Users,
|
Users,
|
||||||
// } from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TYPE_DATE } from "./enum";
|
import { TYPE_DATE } from "./enum";
|
||||||
|
|
||||||
export enum PATH {
|
export enum PATH {
|
||||||
@@ -13,55 +13,41 @@ export enum PATH {
|
|||||||
REGISTER = "/auth/register",
|
REGISTER = "/auth/register",
|
||||||
FORGOT_PASSWORD = "/auth/forgot-password",
|
FORGOT_PASSWORD = "/auth/forgot-password",
|
||||||
|
|
||||||
|
USERS = "/users",
|
||||||
|
|
||||||
PROFILE = "/profile",
|
PROFILE = "/profile",
|
||||||
UPDATEPROFILE = "/profile/update",
|
UPDATEPROFILE = "/profile/update",
|
||||||
|
|
||||||
|
BLUEPRINT = "/blueprint",
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const Menus: {
|
export const Menus: {
|
||||||
// title?: string;
|
title?: string;
|
||||||
// group: {
|
group: {
|
||||||
// path: string;
|
path: string;
|
||||||
// pathActive: string;
|
pathActive: string;
|
||||||
// // isSpecial?: TYPE_SPECIAL;
|
// isSpecial?: TYPE_SPECIAL;
|
||||||
// title: string;
|
title: string;
|
||||||
// icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
// }[];
|
}[];
|
||||||
// }[] = [
|
}[] = [
|
||||||
// {
|
{
|
||||||
// group: [
|
group: [
|
||||||
// {
|
{
|
||||||
// title: "Dashboard",
|
title: "Dashboard",
|
||||||
// icon: CircleGauge,
|
icon: CircleGauge,
|
||||||
// path: PATH.DASHBOARD || PATH.HOME,
|
path: PATH.HOME,
|
||||||
// pathActive: PATH.DASHBOARD,
|
pathActive: PATH.HOME,
|
||||||
// },
|
},
|
||||||
// {
|
{
|
||||||
// title: "Bệnh nhân",
|
title: "Người dùng",
|
||||||
// icon: Users,
|
icon: Users,
|
||||||
// path: PATH.PATIENT,
|
path: PATH.USERS,
|
||||||
// pathActive: PATH.PATIENT,
|
pathActive: PATH.USERS,
|
||||||
// },
|
},
|
||||||
// {
|
],
|
||||||
// title: "Phiên khám",
|
},
|
||||||
// icon: HeartPulse,
|
];
|
||||||
// path: PATH.CONSULTATION,
|
|
||||||
// pathActive: PATH.CONSULTATION,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Đơn thuốc",
|
|
||||||
// icon: Pill,
|
|
||||||
// path: PATH.PRESCRIPTION,
|
|
||||||
// pathActive: PATH.PRESCRIPTION,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// title: "Thống kê chuyên khoa",
|
|
||||||
// icon: ChartNoAxesCombined,
|
|
||||||
// path: PATH.STATISTICAL,
|
|
||||||
// pathActive: PATH.STATISTICAL,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
export const ListOptionFilterDate: {
|
export const ListOptionFilterDate: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -105,47 +91,4 @@ export const ListOptionFilterDate: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SPECIALTY_ORDER = [
|
export const KEY_STORE = "management-tedi";
|
||||||
"Khám Nội",
|
|
||||||
"Khám Răng",
|
|
||||||
"Khám Tai Mũi Họng",
|
|
||||||
"Khám Mắt",
|
|
||||||
"Khám Sản phụ khoa",
|
|
||||||
"Xét nghiệm máu",
|
|
||||||
"Xét nghiệm nước tiểu",
|
|
||||||
"X-quang",
|
|
||||||
"Điện tim",
|
|
||||||
"Siêu Âm",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const SPECIALTY_DISPLAY_NAME: Record<string, string> = {
|
|
||||||
Nội: "Khám Nội",
|
|
||||||
"Khám Nội": "Khám Nội",
|
|
||||||
|
|
||||||
Răng: "Khám Răng",
|
|
||||||
"Khám Răng": "Khám Răng",
|
|
||||||
|
|
||||||
"Tai Mũi Họng": "Khám Tai Mũi Họng",
|
|
||||||
"Khám Tai Mũi Họng": "Khám Tai Mũi Họng",
|
|
||||||
|
|
||||||
Mắt: "Khám Mắt",
|
|
||||||
"Khám Mắt": "Khám Mắt",
|
|
||||||
|
|
||||||
"Sản Phụ Khoa": "Khám Sản phụ khoa",
|
|
||||||
"Sản phụ khoa": "Khám Sản phụ khoa",
|
|
||||||
"Khám Sản phụ khoa": "Khám Sản phụ khoa",
|
|
||||||
|
|
||||||
"Xét nghiệm máu": "Xét nghiệm máu",
|
|
||||||
|
|
||||||
"Xét nghiệm nước tiểu": "Xét nghiệm nước tiểu",
|
|
||||||
|
|
||||||
"X-Quang": "X-quang",
|
|
||||||
"X-quang": "X-quang",
|
|
||||||
|
|
||||||
"Điện Tim": "Điện tim",
|
|
||||||
"Điện tim": "Điện tim",
|
|
||||||
|
|
||||||
"Siêu Âm": "Siêu Âm",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const KEY_STORE = "management-kham-benh";
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import hamburger from "../../../public/static/images/icon-hamburger.svg";
|
||||||
import LogoTedi from "../../../public/static/images/logo-tedi.png";
|
import LogoTedi from "../../../public/static/images/logo-tedi.png";
|
||||||
|
import logoIconTedi from "../../app/favicon.ico";
|
||||||
import full_screen from "../../../public/static/images/full-screen.svg";
|
import full_screen from "../../../public/static/images/full-screen.svg";
|
||||||
import avatar from "../../../public/static/images/avatar-default.png";
|
import avatar from "../../../public/static/images/avatar-default.png";
|
||||||
import tickCircle from "../../../public/static/images/tick-circle.svg";
|
import tickCircle from "../../../public/static/images/tick-circle.svg";
|
||||||
@@ -10,7 +12,9 @@ import emptyTable from "../../../public/static/images/empty_table.png";
|
|||||||
import emptyFile from "../../../public/static/images/emptyFile.svg";
|
import emptyFile from "../../../public/static/images/emptyFile.svg";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
hamburger,
|
||||||
LogoTedi,
|
LogoTedi,
|
||||||
|
logoIconTedi,
|
||||||
full_screen,
|
full_screen,
|
||||||
avatar,
|
avatar,
|
||||||
tickCircle,
|
tickCircle,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import axiosClient from ".";
|
import axiosClient from ".";
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
token: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
userId: string;
|
accessTokenExpiresAt: string;
|
||||||
|
sessionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authServices = {
|
const authServices = {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const refreshAccessToken = async (): Promise<string | null> => {
|
|||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessTokenExpiresAt: string;
|
accessTokenExpiresAt: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
}>("/api/auth/refresh", {
|
}>("/auth/refresh", {
|
||||||
refreshToken,
|
refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -327,20 +327,26 @@ export const httpRequest = async <T>({
|
|||||||
|
|
||||||
/* ================= SUCCESS ================= */
|
/* ================= SUCCESS ================= */
|
||||||
|
|
||||||
if (safeRes.success) {
|
if (
|
||||||
|
safeRes.success === true ||
|
||||||
|
(safeRes.success === undefined && !safeRes.error)
|
||||||
|
) {
|
||||||
if (showMessageSuccess) {
|
if (showMessageSuccess) {
|
||||||
toastSuccess({
|
toastSuccess({
|
||||||
msg: msgSuccess,
|
msg: msgSuccess,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeRes.data as T;
|
// Nếu API có data thì trả data
|
||||||
|
// Nếu API trả trực tiếp object thì trả chính object đó
|
||||||
|
return (safeRes.data !== undefined ? safeRes.data : safeRes) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ================= FAIL ================= */
|
/* ================= FAIL ================= */
|
||||||
|
|
||||||
const error = {
|
const error = {
|
||||||
message: safeRes.error?.message || "Có lỗi xảy ra",
|
message: safeRes.error?.message || "Có lỗi xảy ra",
|
||||||
|
|
||||||
code: safeRes.error?.code,
|
code: safeRes.error?.code,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import axiosClient from ".";
|
||||||
|
|
||||||
|
export interface GetUsersParams {
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
status?: string;
|
||||||
|
roleId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersItems {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
status: number;
|
||||||
|
lockoutEnd: string;
|
||||||
|
roleCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersResponse {
|
||||||
|
items: UsersItems[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersServices = {
|
||||||
|
getUsers: (params?: GetUsersParams, tokentAxios?: any) => {
|
||||||
|
return axiosClient.get<UsersResponse>(`/admin/users`, {
|
||||||
|
params,
|
||||||
|
cancelToken: tokentAxios,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default usersServices;
|
||||||
Reference in New Issue
Block a user