"use client" import { deleteDailyWorkData, getDailyWorkList, lockDailyWorkData, } from "@/components/label/api/daily" import { Daily } from "@/components/label/api/daily/typing" import { getSelfProjectList } from "@/components/label/api/project" import useAuth from "@/components/label/hooks/useAuth" import { useAllUserStore } from "@/components/label/store/auth" import { ActionIcon, Badge, Button, Collapse, Flex, Group, MultiSelect, Paper, SimpleGrid, Stack, Text, TextInput, } from "@mantine/core" import { modals } from "@mantine/modals" import { notifications } from "@mantine/notifications" import { IconChevronDown, IconChevronUp, IconEdit, IconLock, IconLockOpen, IconRefresh, IconSearch, IconTrash, } from "@tabler/icons-react" import dayjs from "dayjs" import { DataTable, DataTableColumn, DataTableSortStatus, } from "mantine-datatable" import { useCallback, useEffect, useMemo, useState } from "react" import * as XLSX from "xlsx-js-style" import DailyReportModal from "../../person/report/components/DailyReportModal" import { accountTypeOpts, actualTypeOpts, objTypeOpts, objTypeTextMap, performanceOpts, performanceTextMap, setTypeOpts, workTypeOpts, } from "../../person/report/config" import { flattenLeaderTree, toNumberArray, weekDayText } from "../utils" function toBoolean(value?: string) { if (value === "true") return true if (value === "false") return false return undefined } function toNumber(value?: string) { if (!value) return undefined const n = Number(value) return Number.isFinite(n) ? n : undefined } const setTypeMap = new Map(setTypeOpts.map((i) => [i.value, i.label])) const actualTypeMap = new Map(actualTypeOpts.map((i) => [i.value, i.label])) const workTypeMap = new Map(workTypeOpts.map((i) => [i.value, i.label])) const accountTypeMap = new Map(accountTypeOpts.map((i) => [i.value, i.label])) export default function TeamDailypaperPage() { const { isShow } = useAuth() const treeData = useAllUserStore((s) => s.treeData) const leaderOptions = useMemo( () => flattenLeaderTree(treeData ?? []), [treeData] ) const defaultFilters = useMemo(() => { return { date_start: dayjs().subtract(6, "day").format("YYYY-MM-DD"), date_end: dayjs().format("YYYY-MM-DD"), uid: [] as string[], set_type: "", actual_type: "", leader_uid: [] as string[], work_type: "", accounting_type: "", project_id: [] as string[], performance_accounting: "", obj_type: "", } }, []) const [projectOptions, setProjectOptions] = useState< Array<{ label: string; value: string }> >([]) const [filters, setFilters] = useState(defaultFilters) const [appliedFilters, setAppliedFilters] = useState(defaultFilters) const [queryTrigger, setQueryTrigger] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [totalItems, setTotalItems] = useState(0) const [loading, setLoading] = useState(false) const [records, setRecords] = useState([]) const [summary, setSummary] = useState(null) const [sortStatus, setSortStatus] = useState({ columnAccessor: "id", direction: "desc", }) const [modalOpened, setModalOpened] = useState(false) const [editingRecord, setEditingRecord] = useState(null) const [searchExpanded, setSearchExpanded] = useState(false) const queryParams = useMemo(() => { const params: Daily.Request = { page_number: page, page_size: pageSize, flag: 1, date_start: appliedFilters.date_start || undefined, date_end: appliedFilters.date_end || undefined, uid: toNumberArray(appliedFilters.uid), set_type: appliedFilters.set_type || undefined, actual_type: appliedFilters.actual_type || undefined, leader_uid: toNumberArray(appliedFilters.leader_uid), work_type: appliedFilters.work_type || undefined, accounting_type: appliedFilters.accounting_type || undefined, project_id: toNumberArray(appliedFilters.project_id), performance_accounting: toBoolean(appliedFilters.performance_accounting), obj_type: toNumber(appliedFilters.obj_type), order_by: `${sortStatus.columnAccessor} ${sortStatus.direction}`, } Object.keys(params).forEach((k) => { const key = k as keyof Daily.Request const value = params[key] if (value === "" || value === null) delete params[key] if (Array.isArray(value) && value.length === 0) delete params[key] if (value !== 0 && value !== false && value !== "" && !value) { delete params[key] } }) return params }, [ appliedFilters, page, pageSize, sortStatus.columnAccessor, sortStatus.direction, ]) const load = useCallback(async () => { if (!queryTrigger) return setLoading(true) try { const res = await getDailyWorkList(queryParams) setRecords(res?.daily_work_list ?? []) setTotalItems(res?.total_items ?? 0) setSummary(res?.total_summary ?? null) } catch (e) { setRecords([]) setTotalItems(0) setSummary(null) notifications.show({ color: "red", title: "加载失败", message: e instanceof Error ? e.message : "请求失败", }) } finally { setLoading(false) } }, [queryParams, queryTrigger]) useEffect(() => { load() }, [load]) useEffect(() => { queueMicrotask(() => setQueryTrigger((v) => v + 1)) }, []) useEffect(() => { const run = async () => { const res = await getSelfProjectList() setProjectOptions( (res ?? []).map((p) => ({ label: p.label, value: String(p.value) })) ) } queueMicrotask(run) }, []) const handleExportData = () => { if (!records.length) { notifications.show({ color: "yellow", title: "暂无数据", message: "当前无可导出日报数据", }) return } const exportTableHeader = [ "员工姓名", "日期", "星期", "规定类型", "实际类型", "任务组长", "工作类别", "核算方式", "项目名称", "绩效核算", "对象类型", "余量正负", "开始时间", "结束时间", "工时", "额定倍率", "标准量", "完成量", "余量", "余量工时", "备注", ] const exportData = records.map((item) => [ item.username ?? "-", item.date ?? "-", item.date ? weekDayText(dayjs(item.date).day()) : "-", setTypeMap.get(item.set_type ?? "") ?? "-", actualTypeMap.get(item.actual_type ?? "") ?? "-", item.leader_name ?? "-", workTypeMap.get(item.work_type ?? "") ?? "-", accountTypeMap.get(item.accounting_type ?? "") ?? "-", item.project_name ?? "-", item.performance_accounting === true ? "是" : item.performance_accounting === false ? "否" : "-", item.obj_type === 0 || item.obj_type === 1 ? (objTypeTextMap.get(item.obj_type) ?? "-") : "-", item.residual_pm === true ? "正" : item.residual_pm === false ? "负" : "-", item.start_time ?? "-", item.end_time ?? "-", item.work_time ?? 0, item.rate ?? 0, item.standard_size ?? 0, item.completed_size ?? 0, item.residual_size ?? 0, item.residual_work_time ?? 0, item.remarks ?? "-", ]) const wb = XLSX.utils.book_new() const ws = XLSX.utils.aoa_to_sheet([exportTableHeader, ...exportData]) XLSX.utils.book_append_sheet(wb, ws, "团队日报数据") XLSX.writeFile(wb, "团队日报数据.xlsx") } const columns = useMemo(() => { const cols: DataTableColumn[] = [ { accessor: "id", title: "ID", width: 80, sortable: true }, { accessor: "username", title: "员工姓名", width: 120, render: (record) => record.username ?? "-", }, { accessor: "date", title: "日期", width: 120, sortable: true, render: (record) => record.date ?? "-", }, { accessor: "week", title: "星期", width: 90, render: (record) => record.date ? weekDayText(dayjs(record.date).day()) : "-", }, { accessor: "set_type", title: "规定类型", width: 120, render: (record) => setTypeMap.get(record.set_type ?? "") ?? "-", }, { accessor: "actual_type", title: "实际类型", width: 120, render: (record) => actualTypeMap.get(record.actual_type ?? "") ?? "-", }, { accessor: "leader_name", title: "任务组长", width: 120 }, { accessor: "work_type", title: "工作类别", width: 140, render: (record) => workTypeMap.get(record.work_type ?? "") ?? "-", }, { accessor: "accounting_type", title: "核算方式", width: 140, render: (record) => accountTypeMap.get(record.accounting_type ?? "") ?? "-", }, { accessor: "project_name", title: "项目名称", width: 220, render: (record) => ( {record.project_name ?? "-"} ), }, { accessor: "performance_accounting", title: "绩效核算", width: 110, render: (record) => { if (record.performance_accounting === true) { return {performanceTextMap.get(true)} } if (record.performance_accounting === false) { return {performanceTextMap.get(false)} } return "-" }, }, { accessor: "obj_type", title: "对象类型", width: 100, render: (record) => { if (record.obj_type === 0 || record.obj_type === 1) { return objTypeTextMap.get(record.obj_type) ?? "-" } return "-" }, }, { accessor: "residual_pm", title: "余量正负", width: 100, render: (record) => record.residual_pm === true ? "正" : record.residual_pm === false ? "负" : "-", }, { accessor: "start_time", title: "开始时间", width: 120 }, { accessor: "end_time", title: "结束时间", width: 120 }, { accessor: "work_time", title: "工时", width: 90, sortable: true }, { accessor: "rate", title: "额定倍率", width: 90 }, { accessor: "standard_size", title: "标准量", width: 90, sortable: true }, { accessor: "completed_size", title: "完成量", width: 90, sortable: true, }, { accessor: "residual_size", title: "余量", width: 90, sortable: true }, { accessor: "residual_work_time", title: "余量工时", width: 110, sortable: true, }, { accessor: "remarks", title: "备注", width: 200, render: (record) => ( {record.remarks ?? "-"} ), }, { accessor: "operation", title: "操作", width: 120, textAlign: "center", render: (record) => { const locked = !!record.lock_status return ( {isShow("edit") ? ( { if (locked) return setEditingRecord(record) setModalOpened(true) }}> ) : null} {isShow("lock") ? ( { if (!record.id) return await lockDailyWorkData(!locked, record.id) notifications.show({ color: "green", title: "操作成功", message: locked ? "已解锁日报" : "已锁定日报", }) load() }}> {locked ? : } ) : null} {isShow("delete") ? ( { if (locked || !record.id) return modals.openConfirmModal({ title: "删除日报", centered: true, children: ( 删除后不可恢复,你确定删除该日报吗? ), labels: { confirm: "删除", cancel: "取消" }, confirmProps: { color: "red" }, onConfirm: async () => { await deleteDailyWorkData(record.id as number) notifications.show({ color: "green", title: "操作成功", message: "已删除日报", }) load() }, }) }}> ) : null} ) }, }, ] return cols }, [isShow, load]) const summaryText = useMemo(() => { return { total_work_time: summary?.total_work_time ?? 0, total_standard_size: summary?.total_standard_size ?? 0, total_completed_size: summary?.total_completed_size ?? 0, total_residual_size: summary?.total_residual_size ?? 0, total_residual_work_time: summary?.total_residual_work_time ?? 0, } }, [summary]) return ( 搜索条件 setFilters((s) => ({ ...s, date_start: e.target.value })) } /> setFilters((s) => ({ ...s, date_end: e.target.value })) } /> setFilters((s) => ({ ...s, uid: v }))} /> setFilters((s) => ({ ...s, project_id: v }))} /> setFilters((s) => ({ ...s, leader_uid: v }))} /> setFilters((s) => ({ ...s, set_type: v[0] ?? "" })) } /> setFilters((s) => ({ ...s, actual_type: v[0] ?? "" })) } /> setFilters((s) => ({ ...s, work_type: v[0] ?? "" })) } /> setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" })) } /> setFilters((s) => ({ ...s, performance_accounting: v[0] ?? "", })) } /> setFilters((s) => ({ ...s, obj_type: v[0] ?? "" })) } /> width="100%" style={{ width: "100%" }} withTableBorder withRowBorders pinFirstColumn pinLastColumn scrollAreaProps={{ type: "auto" }} fetching={loading} records={records} columns={columns} totalRecords={totalItems} recordsPerPage={pageSize} page={page} onPageChange={setPage} onRecordsPerPageChange={(v) => { setPageSize(v) setPage(1) }} recordsPerPageOptions={[10, 15, 20, 50]} noRecordsText="暂无数据" recordsPerPageLabel="当前页数" sortStatus={sortStatus} onSortStatusChange={(s) => { setSortStatus(s) setPage(1) }} /> 当前页总计 工时:{summaryText.total_work_time} 标准量:{summaryText.total_standard_size} 完成量:{summaryText.total_completed_size} 余量:{summaryText.total_residual_size} 余量工时:{summaryText.total_residual_work_time} { setModalOpened(false) setEditingRecord(null) if (refresh) load() }} /> ) }