feat:update
This commit is contained in:
@@ -106,7 +106,7 @@ export default function Table<T>({
|
||||
);
|
||||
};
|
||||
|
||||
/* ================= ROWS ================= */
|
||||
/* ================= DESKTOP / TABLET ROWS ================= */
|
||||
const renderRows = (rows: T[], path: number[] = []): React.ReactNode =>
|
||||
rows.map((row, i) => {
|
||||
const currentPath = [...path, i];
|
||||
@@ -141,6 +141,7 @@ export default function Table<T>({
|
||||
"px-4 py-3 text-sm font-medium whitespace-nowrap",
|
||||
col.fixedLeft && "sticky left-0 bg-white z-10",
|
||||
col.fixedRight && "sticky right-0 bg-white z-10",
|
||||
col.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -171,71 +172,145 @@ export default function Table<T>({
|
||||
);
|
||||
});
|
||||
|
||||
/* ================= UI ================= */
|
||||
return (
|
||||
<div
|
||||
ref={tableRef}
|
||||
className={clsx(
|
||||
"overflow-auto pb-2",
|
||||
fixedHeader && "[&>table>thead]:sticky [&>table>thead]:top-0",
|
||||
activeHeader && "[&>table>thead]:bg-[#F4F7FA]",
|
||||
)}
|
||||
>
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-white border-b">
|
||||
<tr>
|
||||
{getChildren && <th className="w-[60px]" />}
|
||||
/* ================= MOBILE CARDS (KHUÔN BẢNG GIỐNG ẢNH MẪU) ================= */
|
||||
const renderMobileCards = (rows: T[], path: number[] = []): React.ReactNode =>
|
||||
rows.map((row, i) => {
|
||||
const currentPath = [...path, i];
|
||||
const keyPath = useIndexPathAsKey ? currentPath.join(".") : rowKey(row);
|
||||
const children = getChildren?.(row);
|
||||
const expanded = expandedRows.includes(keyPath);
|
||||
|
||||
{column.map((col, i) => {
|
||||
const isActive = col.sortTable === sortConfig.key;
|
||||
|
||||
return (
|
||||
<th
|
||||
key={i}
|
||||
ref={(el) => {
|
||||
thRefs.current[i] = el;
|
||||
}}
|
||||
onClick={() => col.sortTable && handleSort(col.sortTable)}
|
||||
className={clsx(
|
||||
"px-4 py-3 text-left text-[15px] font-semibold whitespace-nowrap",
|
||||
col.sortTable && "cursor-pointer",
|
||||
isActive && "text-blue-600",
|
||||
col.fixedLeft && "sticky left-0 bg-white z-10",
|
||||
col.fixedRight && "sticky right-0 bg-white z-10",
|
||||
return (
|
||||
<Fragment key={String(keyPath)}>
|
||||
{/* Khung Card cho mỗi bệnh nhân / dòng */}
|
||||
<div className="border border-gray-500 bg-white overflow-hidden text-sm rounded-none mb-3">
|
||||
{column.map((col, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
/* Chia 40% - 60% giúp cột chứa button Action rộng rãi hơn */
|
||||
className="grid grid-cols-[2fr_3fr] border-b border-gray-500 last:border-b-0 divide-x divide-gray-500 min-h-[44px]"
|
||||
>
|
||||
{/* Tiêu đề bên trái (TÊN BỆNH NHÂN, MÃ PHIÊN KHÁM,...) */}
|
||||
<div className="p-2 font-bold flex items-center justify-center text-center bg-white uppercase text-xs tracking-tight text-gray-900 leading-tight min-w-0 break-words">
|
||||
{col.checkBox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-1.5 w-4 h-4 accent-blue-500 cursor-pointer shrink-0"
|
||||
onChange={(e) => handleCheckedRow?.(e, row)}
|
||||
checked={handleIsCheckedRow?.(row) ?? false}
|
||||
/>
|
||||
)}
|
||||
{col.title}
|
||||
</div>
|
||||
|
||||
{/* Giá trị / Nút bấm bên phải */}
|
||||
<div className="p-2 flex flex-wrap items-center justify-center text-center gap-1.5 min-w-0 break-words overflow-hidden text-gray-800 font-normal">
|
||||
{col.render(row, i, currentPath)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Xem thêm nếu có hàng con */}
|
||||
{getChildren && !!children?.length && (
|
||||
<div className="p-2 bg-gray-50 border-t border-gray-500 text-center">
|
||||
<button
|
||||
onClick={() => toggleExpand(keyPath)}
|
||||
className="flex items-center justify-center gap-1 w-full text-blue-600 font-medium text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{col.checkBox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={handleCheckedAll}
|
||||
checked={isCheckedAll}
|
||||
/>
|
||||
{expanded ? "Thu gọn" : "Xem thêm chi tiết"}
|
||||
<ChevronUp
|
||||
size={16}
|
||||
className={clsx(
|
||||
"transition-transform",
|
||||
expanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{col.title}
|
||||
{/* Render hàng con nếu có */}
|
||||
{expanded && children && (
|
||||
<div className="pl-2 border-l-2 border-blue-500 space-y-1">
|
||||
{renderMobileCards(children, currentPath)}
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
{col.sortTable &&
|
||||
(isActive ? (
|
||||
sortConfig.direction === "asc" ? (
|
||||
<ArrowDownNarrowWide size={14} />
|
||||
) : sortConfig.direction === "desc" ? (
|
||||
<ArrowDownWideNarrow size={14} />
|
||||
/* ================= UI MAIN ================= */
|
||||
return (
|
||||
<div ref={tableRef} className="w-full">
|
||||
{/* 1. ĐIỆN THOẠI (< md) */}
|
||||
<div className="block md:hidden">{renderMobileCards(sortedData)}</div>
|
||||
|
||||
{/* 2. TABLET & DESKTOP (>= md) */}
|
||||
<div
|
||||
className={clsx(
|
||||
"hidden md:block overflow-auto pb-2",
|
||||
fixedHeader && "[&>table>thead]:sticky [&>table>thead]:top-0",
|
||||
activeHeader && "[&>table>thead]:bg-[#F4F7FA]",
|
||||
)}
|
||||
>
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-white border-b">
|
||||
<tr>
|
||||
{getChildren && <th className="w-[60px]" />}
|
||||
|
||||
{column.map((col, i) => {
|
||||
const isActive = col.sortTable === sortConfig.key;
|
||||
|
||||
return (
|
||||
<th
|
||||
key={i}
|
||||
ref={(el) => {
|
||||
thRefs.current[i] = el;
|
||||
}}
|
||||
onClick={() => col.sortTable && handleSort(col.sortTable)}
|
||||
className={clsx(
|
||||
"px-4 py-3 text-left text-[15px] font-semibold whitespace-nowrap",
|
||||
col.sortTable && "cursor-pointer",
|
||||
isActive && "text-blue-600",
|
||||
col.fixedLeft && "sticky left-0 bg-white z-10",
|
||||
col.fixedRight && "sticky right-0 bg-white z-10",
|
||||
col.className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{col.checkBox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={handleCheckedAll}
|
||||
checked={isCheckedAll}
|
||||
/>
|
||||
)}
|
||||
|
||||
{col.title}
|
||||
|
||||
{col.sortTable &&
|
||||
(isActive ? (
|
||||
sortConfig.direction === "asc" ? (
|
||||
<ArrowDownNarrowWide size={14} />
|
||||
) : sortConfig.direction === "desc" ? (
|
||||
<ArrowDownWideNarrow size={14} />
|
||||
) : (
|
||||
<ArrowDownUp size={14} />
|
||||
)
|
||||
) : (
|
||||
<ArrowDownUp size={14} />
|
||||
)
|
||||
) : (
|
||||
<ArrowDownUp size={14} />
|
||||
))}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
))}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>{renderRows(sortedData)}</tbody>
|
||||
</table>
|
||||
<tbody>{renderRows(sortedData)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -601,13 +601,19 @@ const MainPageConsultation = () => {
|
||||
|
||||
// ===== TAB TẤT CẢ =====
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
/*
|
||||
Chỉnh sửa tại đây:
|
||||
- flex-col: xếp dọc trên màn mobile
|
||||
- sm:flex-row: quay lại xếp ngang trên tablet / desktop (>= 640px)
|
||||
- w-full sm:w-auto: mở rộng full chiều rộng ô khi ở màn mobile cho cân đối
|
||||
*/
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 w-full sm:w-auto">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
className={clsx(
|
||||
"flex h-8 w-8 items-center justify-center rounded-full border border-gray-200",
|
||||
"flex h-8 w-8 items-center justify-center rounded-full border border-gray-200 shrink-0",
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "hover:bg-gray-100",
|
||||
@@ -623,14 +629,14 @@ const MainPageConsultation = () => {
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="
|
||||
z-50
|
||||
min-w-[300px]
|
||||
rounded-xl
|
||||
border
|
||||
bg-white
|
||||
p-2
|
||||
shadow-xl
|
||||
"
|
||||
z-50
|
||||
min-w-[300px]
|
||||
rounded-xl
|
||||
border
|
||||
bg-white
|
||||
p-2
|
||||
shadow-xl
|
||||
"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{item.specialtyClinics.map((specialty) => (
|
||||
@@ -658,6 +664,7 @@ const MainPageConsultation = () => {
|
||||
variant="red"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenCancel(item)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Hủy
|
||||
</CustomButton>
|
||||
@@ -667,6 +674,7 @@ const MainPageConsultation = () => {
|
||||
variant="green"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenComplete(item)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Hoàn thành
|
||||
</CustomButton>
|
||||
|
||||
@@ -300,7 +300,12 @@ const MainPagePatient = () => {
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
/*
|
||||
- flex-col: Xếp dọc các icon trên mobile
|
||||
- sm:flex-row: Xếp ngang lại trên màn lớn (>= 640px)
|
||||
- items-center justify-center: Căn giữa các nút khi xếp dọc
|
||||
*/
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
|
||||
@@ -185,7 +185,7 @@ const MainPrescription = () => {
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: any) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2">
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Eye className="text-blue-400" />}
|
||||
|
||||
Reference in New Issue
Block a user