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 =>
|
const renderRows = (rows: T[], path: number[] = []): React.ReactNode =>
|
||||||
rows.map((row, i) => {
|
rows.map((row, i) => {
|
||||||
const currentPath = [...path, i];
|
const currentPath = [...path, i];
|
||||||
@@ -141,6 +141,7 @@ export default function Table<T>({
|
|||||||
"px-4 py-3 text-sm font-medium whitespace-nowrap",
|
"px-4 py-3 text-sm font-medium whitespace-nowrap",
|
||||||
col.fixedLeft && "sticky left-0 bg-white z-10",
|
col.fixedLeft && "sticky left-0 bg-white z-10",
|
||||||
col.fixedRight && "sticky right-0 bg-white z-10",
|
col.fixedRight && "sticky right-0 bg-white z-10",
|
||||||
|
col.className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -171,71 +172,145 @@ export default function Table<T>({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ================= UI ================= */
|
/* ================= MOBILE CARDS (KHUÔN BẢNG GIỐNG ẢNH MẪU) ================= */
|
||||||
return (
|
const renderMobileCards = (rows: T[], path: number[] = []): React.ReactNode =>
|
||||||
<div
|
rows.map((row, i) => {
|
||||||
ref={tableRef}
|
const currentPath = [...path, i];
|
||||||
className={clsx(
|
const keyPath = useIndexPathAsKey ? currentPath.join(".") : rowKey(row);
|
||||||
"overflow-auto pb-2",
|
const children = getChildren?.(row);
|
||||||
fixedHeader && "[&>table>thead]:sticky [&>table>thead]:top-0",
|
const expanded = expandedRows.includes(keyPath);
|
||||||
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) => {
|
return (
|
||||||
const isActive = col.sortTable === sortConfig.key;
|
<Fragment key={String(keyPath)}>
|
||||||
|
{/* Khung Card cho mỗi bệnh nhân / dòng */}
|
||||||
return (
|
<div className="border border-gray-500 bg-white overflow-hidden text-sm rounded-none mb-3">
|
||||||
<th
|
{column.map((col, idx) => (
|
||||||
key={i}
|
<div
|
||||||
ref={(el) => {
|
key={idx}
|
||||||
thRefs.current[i] = el;
|
/* 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]"
|
||||||
onClick={() => col.sortTable && handleSort(col.sortTable)}
|
>
|
||||||
className={clsx(
|
{/* Tiêu đề bên trái (TÊN BỆNH NHÂN, MÃ PHIÊN KHÁM,...) */}
|
||||||
"px-4 py-3 text-left text-[15px] font-semibold whitespace-nowrap",
|
<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.sortTable && "cursor-pointer",
|
{col.checkBox && (
|
||||||
isActive && "text-blue-600",
|
<input
|
||||||
col.fixedLeft && "sticky left-0 bg-white z-10",
|
type="checkbox"
|
||||||
col.fixedRight && "sticky right-0 bg-white z-10",
|
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">
|
{expanded ? "Thu gọn" : "Xem thêm chi tiết"}
|
||||||
{col.checkBox && (
|
<ChevronUp
|
||||||
<input
|
size={16}
|
||||||
type="checkbox"
|
className={clsx(
|
||||||
onChange={handleCheckedAll}
|
"transition-transform",
|
||||||
checked={isCheckedAll}
|
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 &&
|
/* ================= UI MAIN ================= */
|
||||||
(isActive ? (
|
return (
|
||||||
sortConfig.direction === "asc" ? (
|
<div ref={tableRef} className="w-full">
|
||||||
<ArrowDownNarrowWide size={14} />
|
{/* 1. ĐIỆN THOẠI (< md) */}
|
||||||
) : sortConfig.direction === "desc" ? (
|
<div className="block md:hidden">{renderMobileCards(sortedData)}</div>
|
||||||
<ArrowDownWideNarrow size={14} />
|
|
||||||
|
{/* 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>
|
||||||
<ArrowDownUp size={14} />
|
</th>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
</th>
|
</tr>
|
||||||
);
|
</thead>
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>{renderRows(sortedData)}</tbody>
|
<tbody>{renderRows(sortedData)}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,13 +601,19 @@ const MainPageConsultation = () => {
|
|||||||
|
|
||||||
// ===== TAB TẤT CẢ =====
|
// ===== TAB TẤT CẢ =====
|
||||||
return (
|
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.Root>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={clsx(
|
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
|
isDisabled
|
||||||
? "cursor-not-allowed opacity-50"
|
? "cursor-not-allowed opacity-50"
|
||||||
: "hover:bg-gray-100",
|
: "hover:bg-gray-100",
|
||||||
@@ -623,14 +629,14 @@ const MainPageConsultation = () => {
|
|||||||
align="end"
|
align="end"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
className="
|
className="
|
||||||
z-50
|
z-50
|
||||||
min-w-[300px]
|
min-w-[300px]
|
||||||
rounded-xl
|
rounded-xl
|
||||||
border
|
border
|
||||||
bg-white
|
bg-white
|
||||||
p-2
|
p-2
|
||||||
shadow-xl
|
shadow-xl
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{item.specialtyClinics.map((specialty) => (
|
{item.specialtyClinics.map((specialty) => (
|
||||||
@@ -658,6 +664,7 @@ const MainPageConsultation = () => {
|
|||||||
variant="red"
|
variant="red"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={() => handleOpenCancel(item)}
|
onClick={() => handleOpenCancel(item)}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
Hủy
|
Hủy
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
@@ -667,6 +674,7 @@ const MainPageConsultation = () => {
|
|||||||
variant="green"
|
variant="green"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={() => handleOpenComplete(item)}
|
onClick={() => handleOpenComplete(item)}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
Hoàn thành
|
Hoàn thành
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
|
|||||||
@@ -300,7 +300,12 @@ const MainPagePatient = () => {
|
|||||||
title: "ACTION",
|
title: "ACTION",
|
||||||
|
|
||||||
render: (item: PatientItem) => (
|
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 */}
|
{/* UPDATE */}
|
||||||
<CustomButton
|
<CustomButton
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const MainPrescription = () => {
|
|||||||
title: "ACTION",
|
title: "ACTION",
|
||||||
|
|
||||||
render: (item: any) => (
|
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
|
<CustomButton
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<Eye className="text-blue-400" />}
|
icon={<Eye className="text-blue-400" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user