import { Fragment, useState, type MouseEvent, type ReactNode } from "react" import { ChartColumnIcon, ChevronRightIcon, LayoutGridIcon, MoreHorizontalIcon, SettingsIcon, type LucideIcon } from "lucide-react" import { Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, DashboardContent, DashboardLayout, DashboardHeader, DashboardSidebar, Collapsible, CollapsibleContent, CollapsibleTrigger, DashboardMain, DataTable, DataTableColumnHeader, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, FilterBar, FilterBarActions, FilterBarSearch, PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderTitle, Progress, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, dataTableSelectColumn, useDataTable, type DataTableColumnDef, } from "@hm-design/ui" import { formatDate } from "./sample-users" import { countDashboardWorkFilter, dashboardWorks, dashboardWorkFilters, dashboardWorkStatus, dashboardWorkStyle, matchesDashboardWorkStatus, type DashboardWork, type DashboardWorkFilter } from "./dashboard-work-status" import "./dashboard-composition.css" // Dashboard Mode: internal navigation, Page Header, KPI cards, Filter Bar and Data Table. // Menu and profile data are local to this Story. See docs/patterns/dashboard-layout.md. const photo = "data:image/svg+xml;utf8," + encodeURIComponent('') const profileData = { name: "김한맥", description: "프로젝트 관리자", photo, fallback: "김", } const profileActions: { label: string; destructive?: boolean }[] = [ { label: "프로필 설정" }, { label: "로그아웃", destructive: true }, ] function ProfileAvatar() { return ( {profileData.fallback} ) } function ProfileMenu({ trigger, side = "bottom", align = "start", modal, matchTrigger = false }: { trigger: ReactNode; side?: "top" | "right" | "bottom" | "left"; align?: "start" | "center" | "end"; modal?: boolean; matchTrigger?: boolean }) { return ( {trigger} {profileData.name} {profileActions.map((action) => ( {action.destructive ? : null} {action.label} ))} ) } type Mode = "compact" | "sidebar" | "header-sidebar" type StatusFilter = DashboardWorkFilter const statusFilters = dashboardWorkFilters.map((id) => ({ id, ...dashboardWorkStatus[id] })) // Variant C (ui_viz patterns-variant-c--all) composition: a 48px Compact Header with the title on the left and a floating 40px status // filter bar centred on the header, followed immediately by the content surface. No GNB, LNB or profile. The structure mirrors the // reference (header > brand + status bar [context | 전체 | group]); colours, type and controls are HM. function CompactHeader({ data, status, onStatusChange }: { data: DashboardWork[]; status: StatusFilter; onStatusChange: (status: StatusFilter) => void }) { const count = (id: StatusFilter) => countDashboardWorkFilter(data, id) const filterButton = (filter: (typeof statusFilters)[number]) => ( onStatusChange(filter.id)}> {filter.label} {count(filter.id)}건 ) const [all, ...rest] = statusFilters const workStates = rest.filter((filter) => filter.id !== "issue") const issue = rest.find((filter) => filter.id === "issue")! return ( 프로젝트 대시보드 업무 {filterButton(all)} {workStates.map(filterButton)} {filterButton(issue)} ) } // Dashboard internal menu; kept at every viewport width. type LocalNode = { id: string; label: string; icon?: LucideIcon; children?: LocalNode[] } const localNav = (thirdLevel?: boolean): LocalNode[] => [ { id: "overview", label: "개요", icon: LayoutGridIcon }, { id: "projects", label: "업무 관리", children: [ thirdLevel ? { id: "projects/tasks", label: "업무 현황", children: [ { id: "projects/tasks/all", label: "전체 업무" }, { id: "projects/tasks/mine", label: "담당 업무" }, { id: "projects/tasks/delayed", label: "지연 업무" }, ], } : { id: "projects/tasks", label: "업무 현황" }, { id: "projects/schedule", label: "일정" }, ], }, { id: "reports", label: "리포트", icon: ChartColumnIcon }, { id: "settings", label: "설정", icon: SettingsIcon }, ] const statusBadge = (status: Exclude) => ( {dashboardWorkStatus[status].label} ) const columns: DataTableColumnDef[] = [ dataTableSelectColumn(), { accessorKey: "name", header: ({ column }) => }, { accessorKey: "department", header: "부서" }, { accessorKey: "status", header: "상태", cell: ({ row }) => {statusBadge(row.original.status)}{row.original.issue ? statusBadge("issue") : null}, filterFn: (row, _columnId, value: StatusFilter) => matchesDashboardWorkStatus(row.original, value), enableSorting: false }, { accessorKey: "createdAt", header: ({ column }) => , cell: ({ row }) => formatDate(row.original.createdAt), }, ] function TaskList({ data, loading, compact, selectedStatus = "all", onReset }: { data: DashboardWork[]; loading?: boolean; compact?: boolean; selectedStatus?: StatusFilter; onReset?: () => void }) { const table = useDataTable({ columns, data, getRowId: (row) => row.id }) const [search, setSearch] = useState("") const [status, setStatus] = useState("all") const total = table.getFilteredRowModel().rows.length return ( 업무 목록 {dashboardWorkStatus[compact ? selectedStatus : status as StatusFilter].label} 업무 {total}건 { setSearch(value) table.setGlobalFilter(value || undefined) }} placeholder="업무명·부서 검색" /> {compact ? null : { setStatus(value) table.getColumn("status")?.setFilterValue(value === "all" ? undefined : value) }} > {statusFilters.map((filter) => {filter.label})} } { setSearch("") setStatus("all") table.resetGlobalFilter(true) table.resetColumnFilters(true) onReset?.() }} > 초기화 ) } function Kpis({ data, loading }: { data: DashboardWork[]; loading?: boolean }) { const kpis = statusFilters.map(({ id, label }) => { const value = countDashboardWorkFilter(data, id) const progress = data.length ? Math.round(value / data.length * 100) : 0 return { id, label, value, progress } }) return ( {kpis.map((kpi) => ( {kpi.label} {loading ? null : ( {kpi.id === "all" ? 전체 : statusBadge(kpi.id)} )} {loading ? ( ) : ( {kpi.value} )} ))} ) } export function DashboardScreen({ mode = "compact", data = dashboardWorks, loading, longTitle, wide, thirdLevel, }: { mode?: Mode data?: DashboardWork[] loading?: boolean longTitle?: boolean wide?: boolean thirdLevel?: boolean }) { const [current, setCurrent] = useState(thirdLevel ? "projects/tasks/all" : "projects/tasks") const [compactStatus, setCompactStatus] = useState("all") const compact = mode === "compact" const resultData = compact ? data.filter((row) => matchesDashboardWorkStatus(row, compactStatus)) : data const [openGroups, setOpenGroups] = useState>({}) const go = (id: string) => (event: MouseEvent) => { event.preventDefault() setCurrent(id) } const isCurrent = (id: string) => current === id || current.startsWith(`${id}/`) const nav = localNav(thirdLevel) const dashboardMenu = (nodes: LocalNode[]): ReactNode => nodes.map((node) => node.children ? ( setOpenGroups((state) => ({ ...state, [node.id]: value }))} > {node.label} {dashboardMenu(node.children)} ) : ( {node.icon ? : null} {node.label} ) ) const title = longTitle ? "전사 프로젝트 포트폴리오의 진행 현황과 담당 부서별 업무 상세 현황" : "업무 현황" const sidebar = ( 프로젝트 대시보드 {dashboardMenu(nav)} {profileData.name} } /> ) // Variant C content: heading, then the result card (Page Header's own 20px bottom spacing; no breadcrumb, actions or KPI cards in the reference). const compactMain = ( 업무 현황을 한눈에 확인하세요 setCompactStatus("all")} /> ) const main = compact ? compactMain : ( 홈 프로젝트 업무 현황 {title} 프로젝트별 업무 진행 상황을 확인하고 조회합니다. 다운로드 신규 등록 일괄 수정 보관 {wide ? ( 넓은 내용 페이지를 밀지 않고 카드 안에서 가로 스크롤합니다. ) : null} ) return ( : mode === "header-sidebar" ? 업무 현황 : undefined} sidebar={compact ? undefined : sidebar} > {main} ) }
{kpi.value}