feat:update

This commit is contained in:
TuanVT
2026-07-27 16:09:08 +07:00
parent 04ba0d3118
commit 684d7d2490
4 changed files with 157 additions and 69 deletions
+132 -57
View File
@@ -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" />}