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": {
|
||||
"@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/fiber": "^9.7.0",
|
||||
"@reduxjs/toolkit": "^2.12.0",
|
||||
@@ -29,6 +29,7 @@
|
||||
"react-redux": "^9.3.0",
|
||||
"react-toastify": "^11.1.0",
|
||||
"three": "^0.185.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^4.4.3",
|
||||
"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";
|
||||
|
||||
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";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="">
|
||||
<BaseLayout title="Dashboard">
|
||||
<MainPageHome />
|
||||
</BaseLayout>
|
||||
// <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";
|
||||
|
||||
import React from "react";
|
||||
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
||||
import clsx from "clsx";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
||||
|
||||
import Header from "./componets/Header";
|
||||
import Navbar from "./componets/Navbar";
|
||||
import RequireAuth from "@/components/protected/RequiredAuth";
|
||||
|
||||
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
||||
@@ -12,45 +16,125 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
||||
const [showFull, setShowFull] = React.useState(true);
|
||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
||||
|
||||
return (
|
||||
// <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>
|
||||
const pathname = usePathname();
|
||||
|
||||
{/* MAIN CONTENT */}
|
||||
<main
|
||||
className={clsx(
|
||||
"w-full",
|
||||
"min-w-0",
|
||||
"overflow-x-hidden",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"ml-0",
|
||||
// Blueprint layout đặc biệt
|
||||
const isBlueprint = pathname.startsWith("/blueprint");
|
||||
|
||||
return (
|
||||
<RequireAuth>
|
||||
<ContextBaseLayout
|
||||
value={{
|
||||
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>
|
||||
</div>
|
||||
</ContextBaseLayout>
|
||||
// </RequireAuth>
|
||||
|
||||
{/* =========================
|
||||
SIDEBAR / NAVBAR
|
||||
========================= */}
|
||||
{!isBlueprint && (
|
||||
<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 */}
|
||||
<div
|
||||
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>
|
||||
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
||||
<div
|
||||
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>
|
||||
{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>
|
||||
|
||||
@@ -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">
|
||||
<Image
|
||||
src={icons.avatar}
|
||||
src={
|
||||
// currentUserProfile?.avatarUrl
|
||||
// ? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||
// :
|
||||
icons.avatar
|
||||
}
|
||||
alt="avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
@@ -118,7 +123,10 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||
/>
|
||||
</div>
|
||||
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
||||
{"User admin"}
|
||||
{
|
||||
// currentUserProfile?.fullName ||
|
||||
"User admin"
|
||||
}
|
||||
</p>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { setInfoUser } from "@/redux/reducer/user";
|
||||
import { PATH } from "@/constant/config";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
||||
import { FileBox, LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
||||
|
||||
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
const router = useRouter();
|
||||
@@ -51,6 +51,21 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
return (
|
||||
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
||||
{/* 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 */}
|
||||
<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";
|
||||
import BeamForm from "./components/BeamForm";
|
||||
import { BridgeTreeSidebar } from "@/components/utils/BridgeTreeSidebar";
|
||||
const MainPageHome = () => {
|
||||
return <div>MainPageHome</div>;
|
||||
};
|
||||
|
||||
const Canvas3D = dynamic(
|
||||
() => 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>
|
||||
);
|
||||
}
|
||||
export default MainPageHome;
|
||||
|
||||
@@ -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,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
import { routerServerGlobal } from "next/dist/server/lib/router-utils/router-server-context";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface MenuNode {
|
||||
id: string;
|
||||
@@ -60,6 +62,7 @@ const bridgeTreeData: MenuNode[] = [
|
||||
];
|
||||
|
||||
export const BridgeTreeSidebar: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { activeTab, setActiveTab } = useSuperTStore();
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({
|
||||
superstructure: true,
|
||||
@@ -73,7 +76,9 @@ export const BridgeTreeSidebar: React.FC = () => {
|
||||
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="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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
// "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 {
|
||||
table_list_patient,
|
||||
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,
|
||||
table_list_users,
|
||||
}
|
||||
|
||||
// Draft = 1,
|
||||
// InProgress = 2,
|
||||
// Completed = 3,
|
||||
// Cancelled = 4,
|
||||
export enum TYPE_STATUS {
|
||||
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
|
||||
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)
|
||||
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
|
||||
}
|
||||
// export enum TYPE_STATUS {
|
||||
// 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
|
||||
// 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)
|
||||
// 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
|
||||
// }
|
||||
|
||||
export enum TYPE_GENDER {
|
||||
MALE = "Nam",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// import {
|
||||
// ChartNoAxesCombined,
|
||||
// CircleGauge,
|
||||
// HeartPulse,
|
||||
// Pill,
|
||||
// Users,
|
||||
// } from "lucide-react";
|
||||
import {
|
||||
ChartNoAxesCombined,
|
||||
CircleGauge,
|
||||
HeartPulse,
|
||||
Pill,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { TYPE_DATE } from "./enum";
|
||||
|
||||
export enum PATH {
|
||||
@@ -13,55 +13,41 @@ export enum PATH {
|
||||
REGISTER = "/auth/register",
|
||||
FORGOT_PASSWORD = "/auth/forgot-password",
|
||||
|
||||
USERS = "/users",
|
||||
|
||||
PROFILE = "/profile",
|
||||
UPDATEPROFILE = "/profile/update",
|
||||
|
||||
BLUEPRINT = "/blueprint",
|
||||
}
|
||||
|
||||
// export const Menus: {
|
||||
// title?: string;
|
||||
// group: {
|
||||
// path: string;
|
||||
// pathActive: string;
|
||||
// // isSpecial?: TYPE_SPECIAL;
|
||||
// title: string;
|
||||
// icon: React.ElementType;
|
||||
// }[];
|
||||
// }[] = [
|
||||
// {
|
||||
// group: [
|
||||
// {
|
||||
// title: "Dashboard",
|
||||
// icon: CircleGauge,
|
||||
// path: PATH.DASHBOARD || PATH.HOME,
|
||||
// pathActive: PATH.DASHBOARD,
|
||||
// },
|
||||
// {
|
||||
// title: "Bệnh nhân",
|
||||
// icon: Users,
|
||||
// path: PATH.PATIENT,
|
||||
// pathActive: PATH.PATIENT,
|
||||
// },
|
||||
// {
|
||||
// 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 Menus: {
|
||||
title?: string;
|
||||
group: {
|
||||
path: string;
|
||||
pathActive: string;
|
||||
// isSpecial?: TYPE_SPECIAL;
|
||||
title: string;
|
||||
icon: React.ElementType;
|
||||
}[];
|
||||
}[] = [
|
||||
{
|
||||
group: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
icon: CircleGauge,
|
||||
path: PATH.HOME,
|
||||
pathActive: PATH.HOME,
|
||||
},
|
||||
{
|
||||
title: "Người dùng",
|
||||
icon: Users,
|
||||
path: PATH.USERS,
|
||||
pathActive: PATH.USERS,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ListOptionFilterDate: {
|
||||
name: string;
|
||||
@@ -105,47 +91,4 @@ export const ListOptionFilterDate: {
|
||||
},
|
||||
];
|
||||
|
||||
export const SPECIALTY_ORDER = [
|
||||
"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";
|
||||
export const KEY_STORE = "management-tedi";
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import hamburger from "../../../public/static/images/icon-hamburger.svg";
|
||||
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 avatar from "../../../public/static/images/avatar-default.png";
|
||||
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";
|
||||
|
||||
export default {
|
||||
hamburger,
|
||||
LogoTedi,
|
||||
logoIconTedi,
|
||||
full_screen,
|
||||
avatar,
|
||||
tickCircle,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userId: string;
|
||||
accessTokenExpiresAt: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
const authServices = {
|
||||
|
||||
@@ -128,7 +128,7 @@ const refreshAccessToken = async (): Promise<string | null> => {
|
||||
refreshToken: string;
|
||||
accessTokenExpiresAt: string;
|
||||
sessionId: string;
|
||||
}>("/api/auth/refresh", {
|
||||
}>("/auth/refresh", {
|
||||
refreshToken,
|
||||
});
|
||||
|
||||
@@ -327,20 +327,26 @@ export const httpRequest = async <T>({
|
||||
|
||||
/* ================= SUCCESS ================= */
|
||||
|
||||
if (safeRes.success) {
|
||||
if (
|
||||
safeRes.success === true ||
|
||||
(safeRes.success === undefined && !safeRes.error)
|
||||
) {
|
||||
if (showMessageSuccess) {
|
||||
toastSuccess({
|
||||
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 ================= */
|
||||
|
||||
const error = {
|
||||
message: safeRes.error?.message || "Có lỗi xảy ra",
|
||||
|
||||
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