feat(FI-08): Add legal document#13
Conversation
| import { Bell, User, Search } from "lucide-react"; | ||
| import { Sidebar } from "./Sidebar"; | ||
| import { AddDocumentForm } from "./AddDocumentForm"; | ||
| import { Toast } from "./Toast"; | ||
| import { useToast } from "../hooks/useToast"; | ||
|
|
||
| /** | ||
| * Full-page layout: sidebar (left) + content area (right). | ||
| * Occupies the full viewport height. | ||
| */ | ||
| export function AddDocumentPage() { | ||
| const toast = useToast(); | ||
|
|
||
| return ( | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| height: "100vh", | ||
| overflow: "hidden", | ||
| fontFamily: | ||
| "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif", | ||
| }} | ||
| > | ||
| {/* Left sidebar */} | ||
| <Sidebar /> | ||
|
|
||
| {/* Right content */} | ||
| <main | ||
| style={{ | ||
| flex: 1, | ||
| background: "#f5f6f8", | ||
| padding: "28px 32px", | ||
| overflowY: "auto", | ||
| }} | ||
| > | ||
| {/* Top bar */} | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| justifyContent: "space-between", | ||
| alignItems: "center", | ||
| marginBottom: "24px", | ||
| }} | ||
| > | ||
| {/* Search */} | ||
| <div style={{ position: "relative" }}> | ||
| <Search | ||
| size={14} | ||
| color="#9ca3af" | ||
| style={{ | ||
| position: "absolute", | ||
| left: "10px", | ||
| top: "50%", | ||
| transform: "translateY(-50%)", | ||
| }} | ||
| /> | ||
| <input | ||
| type="search" | ||
| placeholder="Search legal templates, statutes, or case laws..." | ||
| style={{ | ||
| background: "#fff", | ||
| border: "1px solid #e5e7eb", | ||
| borderRadius: "6px", | ||
| height: "34px", | ||
| fontSize: "13px", | ||
| width: "280px", | ||
| paddingLeft: "32px", | ||
| paddingRight: "12px", | ||
| outline: "none", | ||
| color: "#374151", | ||
| boxSizing: "border-box", | ||
| }} | ||
| /> | ||
| </div> | ||
|
|
||
| {/* Right icons */} | ||
| <div style={{ display: "flex", alignItems: "center", gap: "16px" }}> | ||
| <Bell size={18} color="#6b7280" style={{ cursor: "pointer" }} /> | ||
| <User size={18} color="#6b7280" style={{ cursor: "pointer" }} /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Form card */} | ||
| <div | ||
| style={{ | ||
| background: "#ffffff", | ||
| borderRadius: "12px", | ||
| border: "1px solid #e5e7eb", | ||
| padding: "28px 28px 24px", | ||
| }} | ||
| > | ||
| {/* Card header */} | ||
| <h1 | ||
| style={{ | ||
| fontSize: "22px", | ||
| fontWeight: 700, | ||
| color: "#1e3a8a", | ||
| margin: "0 0 4px", | ||
| fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif", | ||
| letterSpacing: "-0.01em", | ||
| }} | ||
| > | ||
| Thêm văn bản pháp luật | ||
| </h1> | ||
| <p | ||
| style={{ | ||
| fontSize: "13px", | ||
| color: "#6b7280", | ||
| margin: "0 0 24px", | ||
| }} | ||
| > | ||
| Nhập thông tin văn bản để phục vụ tra cứu và AI phân tích | ||
| </p> | ||
|
|
||
| <AddDocumentForm onSuccess={toast.show} /> | ||
| </div> | ||
| </main> | ||
|
|
||
| {/* Toast notification */} | ||
| <Toast | ||
| visible={toast.visible} | ||
| entering={toast.entering} | ||
| exiting={toast.exiting} | ||
| onDismiss={toast.dismiss} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
cái này page thì phải nằm ở folder page chứ em, em move qua nha
| import { useState } from "react"; | ||
| import { Bell, Search } from "lucide-react"; | ||
| import { Sidebar } from "./Sidebar"; | ||
| import { FilterBar } from "./FilterBar"; | ||
| import { DocumentTable } from "./DocumentTable"; | ||
| import { MOCK_DOCUMENTS, LegalDocument } from "../constants/mockDocuments"; | ||
|
|
||
| /** | ||
| * UC-6: Legal Document Management (Admin) page. | ||
| * Displays a searchable, filterable list of legal documents | ||
| * with an expandable version history per row. | ||
| */ | ||
| export function DocumentListPage() { | ||
| const [documents, setDocuments] = useState<LegalDocument[]>(MOCK_DOCUMENTS); | ||
| const [search, setSearch] = useState(""); | ||
|
|
||
| const filtered = documents.filter( | ||
| (d) => | ||
| d.tenVanBan.toLowerCase().includes(search.toLowerCase()) || | ||
| d.soHieu.toLowerCase().includes(search.toLowerCase()) | ||
| ); | ||
|
|
||
| const handleToggle = (id: number) => { | ||
| setDocuments((prev) => | ||
| prev.map((d) => (d.id === id ? { ...d, active: !d.active } : d)) | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| height: "100vh", | ||
| overflow: "hidden", | ||
| fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif", | ||
| }} | ||
| > | ||
| {/* Left sidebar */} | ||
| <Sidebar /> | ||
|
|
||
| {/* Right content */} | ||
| <main | ||
| style={{ | ||
| flex: 1, | ||
| background: "#f5f6f8", | ||
| padding: "28px 32px", | ||
| overflowY: "auto", | ||
| }} | ||
| > | ||
| {/* Top bar */} | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| justifyContent: "space-between", | ||
| alignItems: "center", | ||
| marginBottom: "24px", | ||
| }} | ||
| > | ||
| <div style={{ position: "relative" }}> | ||
| <Search | ||
| size={14} | ||
| color="#9ca3af" | ||
| style={{ | ||
| position: "absolute", | ||
| left: "10px", | ||
| top: "50%", | ||
| transform: "translateY(-50%)", | ||
| }} | ||
| /> | ||
| <input | ||
| type="search" | ||
| placeholder="Search legal templates, statutes, or case laws..." | ||
| style={{ | ||
| background: "#fff", | ||
| border: "1px solid #e5e7eb", | ||
| borderRadius: "6px", | ||
| height: "34px", | ||
| fontSize: "13px", | ||
| width: "280px", | ||
| paddingLeft: "32px", | ||
| paddingRight: "12px", | ||
| outline: "none", | ||
| color: "#374151", | ||
| boxSizing: "border-box", | ||
| }} | ||
| /> | ||
| </div> | ||
| <div style={{ display: "flex", alignItems: "center", gap: "16px" }}> | ||
| <Bell size={18} color="#6b7280" /> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Page heading */} | ||
| <h1 | ||
| style={{ | ||
| fontSize: "22px", | ||
| fontWeight: 700, | ||
| color: "#1a2d6b", | ||
| margin: "0 0 2px", | ||
| letterSpacing: "-0.02em", | ||
| lineHeight: 1.25, | ||
| fontFamily: "'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif", | ||
| }} | ||
| > | ||
| Quản lý văn bản pháp luật | ||
| </h1> | ||
| <p | ||
| style={{ | ||
| fontSize: "13px", | ||
| color: "#6b7280", | ||
| margin: "0 0 20px", | ||
| }} | ||
| > | ||
| Cập nhật và điều chỉnh các văn bản pháp quy trong hệ thống. | ||
| </p> | ||
|
|
||
| {/* White card */} | ||
| <div | ||
| style={{ | ||
| background: "#fff", | ||
| borderRadius: "8px", | ||
| border: "1px solid #e5e7eb", | ||
| overflow: "hidden", | ||
| }} | ||
| > | ||
| <div style={{ padding: "12px 16px", borderBottom: "1px solid #f3f4f6" }}> | ||
| <FilterBar search={search} onSearchChange={setSearch} /> | ||
| </div> | ||
|
|
||
| {/* Document table */} | ||
| <DocumentTable documents={filtered} onToggle={handleToggle} /> | ||
| </div> | ||
| </main> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
này nó cũng là page nên em move qua folder page nha
| } | ||
| `}</style> | ||
| </form> | ||
| ); |
There was a problem hiding this comment.
form thì e tạo một folder form rồi e move nào, component này mục địch là tách phần nhỏ để tái sử dụng e
| type="text" | ||
| aria-invalid={!!errors.soHieuVanBan} | ||
| data-error={!!errors.soHieuVanBan} | ||
| {...register("soHieuVanBan", { |
There was a problem hiding this comment.
A thấy e nên lưu register("soHieuVanBan") vào một biến thay vì e gọi nhiều lần để nó improve readability vs maintainability. Vì hiện tại em đang define handler ở 2 chỗ cho cùng một field:
{...register("soHieuVanBan", {
onBlur: (e) => checkDuplicate(e.target.value),
})}
onChange={(e) => {
register("soHieuVanBan").onChange(e);
clearDuplicate();
}}
Em refactor kiểu này nhìn nó clean hơn nè:
const soHieuField = register("soHieuVanBan", {
onBlur: (e) => checkDuplicate(e.target.value),
});
<input
{...soHieuField}
onChange={(e) => {
soHieuField.onChange(e);
clearDuplicate();
}}
/>
| overflowY: "auto", | ||
| }} | ||
| > | ||
| {/* Top bar */} |
There was a problem hiding this comment.
A thấy phần layout đang bị duplicate giữa các page, đoạn Sidebar, Topbar với phần wrapper main. Mấy phần này có thể tách ra thành một dashboar layout riêng để nó tránh lặp code vs dễ maintain hơn khi sau này cần update UI, layout chung
| }} | ||
| /> | ||
| <input | ||
| type="search" |
There was a problem hiding this comment.
này chưa hoạt đồng nè em, input ni ko có value ko có onchange
| <tbody> | ||
| {documents.map((doc) => ( | ||
| <> | ||
| <tr key={doc.id} style={{ background: "#fff" }}> |
There was a problem hiding this comment.
A thấy đoạn documents.map() này đang dùng fragment nhưng chưa có key ở root element:
{documents.map((doc) => (
<>
Trong React thì key em nên đặt ở phần tử root của vòng map để reconciliation hoạt động ổn định hơn á với tránh warning về sau. Em sửa như ri nè:
{documents.map((doc) => (
<React.Fragment key={doc.id}>
hoặc ri
PhamVanTien1910
left a comment
There was a problem hiding this comment.
- Em move từ inline styling qua tailwind nha
- Chia component để tái sử dụng, tại vì anh thấy code e có nhiều cái lặp nhau với có thể tách component được
- Fix những cái anh review chừng đó trước. Rồi đẩy lên cho anh
UC-5: Add legal document
[FI-08] Tạo UI của UC5 đến UC7
( PHƯƠNG(Frontend)