This commit is contained in:
TuanVT
2026-10-26 16:03:54 +07:00
parent a5ef1901da
commit 0d7d204916
28 changed files with 8932 additions and 1287 deletions
+6568
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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"
}, },
+16
View File
@@ -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
View File
@@ -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>
+16
View File
@@ -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,8 +16,13 @@ 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);
const pathname = usePathname();
// Blueprint layout đặc biệt
const isBlueprint = pathname.startsWith("/blueprint");
return ( return (
// <RequireAuth> <RequireAuth>
<ContextBaseLayout <ContextBaseLayout
value={{ value={{
showFull, showFull,
@@ -22,35 +31,110 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
setOpenMenuMobile, setOpenMenuMobile,
}} }}
> >
<div className="min-h-screen w-full overflow-x-hidden bg-[#f8f8f8]"> <div className="min-h-screen bg-[#f8f8f8]">
{/* HEADER */} {/* =========================
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)}
/>
)}
{/* =========================
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 <header
className={clsx( className={clsx(
"top-0 right-0 z-[11] h-[68px]", "top-0 right-0 h-[68px]",
"w-full", "bg-white border-b border-[#f4f7fa]",
"border-b border-[#f4f7fa]", "z-[11]",
"bg-white",
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", "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 title={title} breadcrumb={breadcrumb} />
</header> </header>
{/* MAIN CONTENT */} {/* =========================
MAIN CONTENT
========================= */}
<main <main
className={clsx( className={clsx(
"w-full",
"min-w-0",
"overflow-x-hidden",
"transition-all duration-300 ease-in-out", "transition-all duration-300 ease-in-out",
isBlueprint
? [
// Blueprint dùng toàn bộ màn hình
"ml-0", "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} {children}
</main> </main>
</div> </div>
</ContextBaseLayout> </ContextBaseLayout>
// </RequireAuth> </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";
+6 -1
View File
@@ -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>
+66
View File
@@ -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>
// );
// };
+9 -24
View File
@@ -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",
+39 -96
View File
@@ -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";
+4
View File
@@ -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,
+3 -2
View File
@@ -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 = {
+9 -3
View File
@@ -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,
}; };
+38
View File
@@ -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;
+1003 -861
View File
File diff suppressed because it is too large Load Diff