"use client" import { TimeFilter } from "@/components/label/api/workload/typing" import { SettingContentPanel, SettingDataTable, SettingFilterActions, SettingFilterPanel, SettingHeaderActions, SettingInlineFilterField, SettingListHeader, settingSurfaceClassNames, } from "@/components/setting/PageSurface" import { Button, Collapse, Stack, TextInput } from "@mantine/core" import { DateInput } from "@mantine/dates" import { notifications } from "@mantine/notifications" import { IconChevronDown, IconChevronUp, IconDownload, IconRefresh, IconSearch, } from "@tabler/icons-react" import dayjs from "dayjs" import "dayjs/locale/zh-cn" import { DataTableColumn, DataTableSortStatus } from "mantine-datatable" import { useEffect, useMemo, useState } from "react" import * as XLSX from "xlsx-js-style" import { DimensionType, GroupedData, MetricColumn } from "./config" function emptyGroupedData(): GroupedData { return { all: [], order_by_group_name: [], order_by_project_name: [], order_by_project_type: [], order_by_user_name: [], } } function createInitialFilters() { const currentDate = dayjs().subtract(1, "day").format("YYYY-MM-DD") return { startDate: currentDate, endDate: currentDate, dimension: "all" as DimensionType, projectName: "", projectType: "", userName: "", groupName: "", } } export default function WorkloadMetricsTable(props: { title: string sheetName: string fileName: string metrics: MetricColumn[] fetchData: (params: TimeFilter) => Promise }) { const { title, sheetName, fileName, metrics, fetchData } = props const [filters, setFilters] = useState(() => createInitialFilters()) const [appliedFilters, setAppliedFilters] = useState(() => createInitialFilters() ) const [data, setData] = useState(emptyGroupedData()) const [loading, setLoading] = useState(false) const [sortStatus, setSortStatus] = useState({ columnAccessor: "work_time", direction: "desc", }) const refresh = async (nextFilters = appliedFilters) => { if (!nextFilters.startDate || !nextFilters.endDate) { notifications.show({ color: "yellow", title: "参数不完整", message: "请选择起止日期", }) return } try { setLoading(true) const res = await fetchData({ start_date: nextFilters.startDate, end_date: nextFilters.endDate, }) setData({ ...emptyGroupedData(), ...(res ?? {}), }) } catch (e) { notifications.show({ color: "red", title: "加载失败", message: e instanceof Error ? e.message : "请求失败", }) setData(emptyGroupedData()) } finally { setLoading(false) } } useEffect(() => { const initialFilters = createInitialFilters() queueMicrotask(() => refresh(initialFilters)) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const filteredRecords = useMemo(() => { let records = [...(data[appliedFilters.dimension] ?? [])] if (appliedFilters.projectName.trim()) { records = records.filter((item) => String(item.project_name ?? "") .toLowerCase() .includes(appliedFilters.projectName.trim().toLowerCase()) ) } if (appliedFilters.projectType.trim()) { records = records.filter((item) => String(item.project_type ?? "") .toLowerCase() .includes(appliedFilters.projectType.trim().toLowerCase()) ) } if (appliedFilters.userName.trim()) { records = records.filter((item) => String(item.user_name ?? "") .toLowerCase() .includes(appliedFilters.userName.trim().toLowerCase()) ) } if (appliedFilters.groupName.trim()) { records = records.filter((item) => String(item.group_name ?? "") .toLowerCase() .includes(appliedFilters.groupName.trim().toLowerCase()) ) } return records }, [appliedFilters, data]) const sortedRecords = useMemo(() => { const list = [...filteredRecords] const accessor = sortStatus.columnAccessor const dir = sortStatus.direction === "asc" ? 1 : -1 list.sort((a, b) => { const av = a?.[accessor as string] const bv = b?.[accessor as string] const an = Number(av) const bn = Number(bv) if (Number.isFinite(an) && Number.isFinite(bn)) { return (an - bn) * dir } return String(av ?? "").localeCompare(String(bv ?? "")) * dir }) return list }, [filteredRecords, sortStatus.columnAccessor, sortStatus.direction]) const baseColumns = useMemo(() => { const allCols: Record[]> = { all: [ { accessor: "project_name", title: "项目名称", width: 180 }, { accessor: "project_type", title: "项目类型", width: 280 }, { accessor: "user_name", title: "用户名", width: 120 }, { accessor: "group_name", title: "用户组", width: 160 }, ], order_by_group_name: [ { accessor: "group_name", title: "用户组", width: 180 }, ], order_by_project_name: [ { accessor: "project_name", title: "项目名称", width: 220 }, ], order_by_project_type: [ { accessor: "project_type", title: "项目类型", width: 320 }, ], order_by_user_name: [ { accessor: "user_name", title: "用户名", width: 180 }, ], } return allCols[appliedFilters.dimension] }, [appliedFilters.dimension]) const columns = useMemo(() => { const totalsByKey = Object.fromEntries( metrics.map((metric) => { const sum = sortedRecords.reduce((acc, item) => { const value = Number(item?.[metric.key] ?? 0) return acc + (Number.isFinite(value) ? value : 0) }, 0) return [metric.key, sum] }) ) as Record const metricCols = metrics.map((metric) => { return { accessor: metric.key, title: metric.title, width: 140, sortable: true, footer: totalsByKey[metric.key] ?? 0, } as DataTableColumn }) return [ { accessor: "index", title: "序号", width: 80, footer: "总计", render: (_record: any, index: number) => index + 1, } as DataTableColumn, ...baseColumns, ...metricCols, ] }, [baseColumns, metrics, sortedRecords]) const handleExport = () => { if (!sortedRecords.length) { notifications.show({ color: "yellow", title: "暂无数据", message: "当前筛选条件下无可导出数据", }) return } const baseHeaders = baseColumns.map((c) => String(c.title ?? c.accessor)) const metricHeaders = metrics.map((m) => m.title) const header = ["序号", ...baseHeaders, ...metricHeaders] const baseAccessors = baseColumns.map((c) => String(c.accessor)) const rows = sortedRecords.map((item, index) => { return [ index + 1, ...baseAccessors.map((key) => item?.[key] ?? "-"), ...metrics.map((m) => item?.[m.key] ?? 0), ] }) const wb = XLSX.utils.book_new() const ws = XLSX.utils.aoa_to_sheet([header, ...rows]) XLSX.utils.book_append_sheet(wb, ws, sheetName) XLSX.writeFile(wb, fileName) } const rowKey = useMemo(() => { if (appliedFilters.dimension === "all") { return (record: any) => `${record.project_id ?? "p"}_${record.uid ?? "u"}_${record.project_name ?? ""}_${record.user_name ?? ""}` } if (appliedFilters.dimension === "order_by_group_name") return "group_name" if (appliedFilters.dimension === "order_by_project_name") return "project_name" if (appliedFilters.dimension === "order_by_project_type") return "project_type" return "user_name" }, [appliedFilters.dimension]) const handleSearch = () => { setAppliedFilters(filters) void refresh(filters) } const handleReset = () => { const nextFilters = createInitialFilters() setFilters(nextFilters) setAppliedFilters(nextFilters) void refresh(nextFilters) } const [searchExpanded, setSearchExpanded] = useState(false) return (
setFilters((s) => ({ ...s, startDate: dayjs(e).format("YYYY-MM-DD"), })) } /> setFilters((s) => ({ ...s, endDate: dayjs(e).format("YYYY-MM-DD"), })) } /> setFilters((prev) => ({ ...prev, projectName: e.target.value, })) } />
setFilters((prev) => ({ ...prev, projectType: e.target.value, })) } />
} /> fetching={loading} records={sortedRecords} columns={columns} idAccessor={rowKey as any} minHeight={0} scrollAreaProps={{ type: "auto" }} style={{ width: "100%", flex: 1, minWidth: 0, minHeight: 0 }} sortStatus={sortStatus} onSortStatusChange={setSortStatus} />
) }