feat(person): add pages
This commit is contained in:
282
app/person/dashboard/components/Icon.tsx
Normal file
282
app/person/dashboard/components/Icon.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
"use client"
|
||||
|
||||
export const DataIcon = () => (
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M0.1 4C0.1 1.84609 1.84609 0.1 4 0.1H28C30.1539 0.1 31.9 1.84609 31.9 4V28C31.9 30.1539 30.1539 31.9 28 31.9H4C1.84609 31.9 0.1 30.1539 0.1 28V4Z"
|
||||
fill="url(#paint0_linear_726_9281)"
|
||||
stroke="url(#paint1_linear_726_9281)"
|
||||
strokeWidth="0.2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8.67848 12.5511L9.89873 11.2781L10.6614 12.0738L9.44114 13.3468L10.2038 14.1424L11.424 12.7898L12.1867 13.5855L10.9665 14.8585L11.1953 15.495L15.2373 11.3577L12.4918 8.25471C12.0342 7.8569 11.1953 7.93646 10.8139 8.41384L8.22088 10.9599C7.83955 11.3577 7.99209 11.9146 8.37341 12.3124L8.67848 12.5511ZM8.37341 23.0535C8.29715 23.3717 8.52595 23.6104 8.83101 23.5309L13.4832 22.0192L9.89873 18.3593L8.37341 23.0535ZM23.5503 11.9942C23.9316 11.5964 23.9316 11.0394 23.5503 10.6416L21.3386 8.41384C20.9573 8.01602 20.3472 8.01602 19.9658 8.41384L18.3642 10.0051L21.9487 13.5855L23.5503 11.9942ZM17.8304 10.5621L10.4326 17.8819L14.0171 21.4622L21.4149 14.0628L17.8304 10.5621ZM23.7791 19.3936L20.4234 16.1315L16.3051 20.1892L17.0677 20.9848L18.3642 19.7118L19.1269 20.5075L17.8304 21.7805L18.593 22.5761L19.8896 21.3031L20.6522 22.0987L19.3557 23.3717L19.5845 23.6104C19.9658 24.0083 20.5759 24.1674 21.0335 23.7696L23.6266 21.2235C24.0079 20.6666 24.1604 19.7914 23.7791 19.3936Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_726_9281"
|
||||
x1="-4"
|
||||
y1="-9.5"
|
||||
x2="29"
|
||||
y2="32"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="white" stopOpacity="0.26" />
|
||||
<stop offset="1" stopColor="#FFCBCE" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_726_9281"
|
||||
x1="1.67638e-07"
|
||||
y1="32"
|
||||
x2="32"
|
||||
y2="1.67638e-07"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#FBACB3" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const HourIcon = () => (
|
||||
<svg
|
||||
width="38"
|
||||
height="37"
|
||||
viewBox="0 0 38 37"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd_726_9271)">
|
||||
<rect
|
||||
x="2"
|
||||
y="1"
|
||||
width="32"
|
||||
height="32"
|
||||
rx="3.84"
|
||||
fill="url(#paint0_linear_726_9271)"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="2"
|
||||
y="1"
|
||||
width="32"
|
||||
height="32"
|
||||
rx="3.84"
|
||||
fill="white"
|
||||
fillOpacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M15.1422 18.626L16.6317 17.5382L20.6829 11.3447L21.6931 9.79101C21.7472 9.70607 21.774 9.60659 21.7699 9.50597C21.7656 9.42473 21.7414 9.34579 21.6995 9.27612C21.6575 9.20644 21.599 9.14817 21.5291 9.10646L20.6829 8.61169C20.5676 8.54209 20.43 8.51976 20.2987 8.54935C20.1674 8.57894 20.0526 8.65817 19.9784 8.77046L19.2562 9.87151L18.9667 10.3242L15.0322 16.7614L15.1429 18.626H15.1422ZM23.0216 9.77994H25.2333C25.7243 9.77994 26.1231 10.2193 26.1231 10.6956V21.9084C26.1231 22.3847 25.7243 22.7709 25.2333 22.7709H10.7668C10.2757 22.7709 9.87695 22.3847 9.87695 21.9084V10.6956C9.87695 10.2193 10.1362 9.77994 10.6272 9.77994H17.8893L13.8034 16.4165L14.0692 20.4736L17.8782 17.903L23.0216 9.77994ZM18.6115 19.4685C18.4533 19.4685 18.3017 19.5314 18.1898 19.6432C18.078 19.755 18.0152 19.9067 18.0152 20.0649C18.0152 20.223 18.078 20.3747 18.1898 20.4865C18.3017 20.5983 18.4533 20.6612 18.6115 20.6612H24.4896C24.6478 20.6612 24.7995 20.5983 24.9113 20.4865C25.0231 20.3747 25.0859 20.223 25.0859 20.0649C25.0859 19.9067 25.0231 19.755 24.9113 19.6432C24.7995 19.5314 24.6478 19.4685 24.4896 19.4685H18.6115ZM25.1963 24.0854H10.8052C10.3606 24.0854 10.0003 24.3941 10.0003 24.7744C10.0003 25.1547 10.3606 25.4634 10.8052 25.4634H25.1963C25.6416 25.4634 26.0013 25.1547 26.0013 24.7744C25.999 24.397 25.6394 24.0854 25.1963 24.0854Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_dd_726_9271"
|
||||
x="0.56"
|
||||
y="0.0399997"
|
||||
width="37.28"
|
||||
height="36.8"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology
|
||||
radius="0.48"
|
||||
operator="erode"
|
||||
in="SourceAlpha"
|
||||
result="effect1_dropShadow_726_9271"
|
||||
/>
|
||||
<feOffset dx="1.44" dy="1.44" />
|
||||
<feGaussianBlur stdDeviation="1.44" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.0627451 0 0 0 0 0.760784 0 0 0 0 0.423529 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_726_9271"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology
|
||||
radius="0.48"
|
||||
operator="erode"
|
||||
in="SourceAlpha"
|
||||
result="effect2_dropShadow_726_9271"
|
||||
/>
|
||||
<feOffset dx="-0.48" />
|
||||
<feGaussianBlur stdDeviation="0.72" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.0627451 0 0 0 0 0.760784 0 0 0 0 0.423529 0 0 0 0.33 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_dropShadow_726_9271"
|
||||
result="effect2_dropShadow_726_9271"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect2_dropShadow_726_9271"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_726_9271"
|
||||
x1="-13"
|
||||
y1="-27"
|
||||
x2="27"
|
||||
y2="31.5"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#E7F9F0" />
|
||||
<stop offset="1" stopColor="#4FD796" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ReviewIcon = () => (
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="3.84" fill="url(#paint0_linear_0_1)" />
|
||||
<g filter="url(#filter0_ii_0_1)">
|
||||
<rect width="32" height="32" rx="3.84" fill="white" fillOpacity="0.1" />
|
||||
</g>
|
||||
<g filter="url(#filter1_ii_0_1)">
|
||||
<rect width="32" height="32" rx="3.84" fill="white" fillOpacity="0.1" />
|
||||
</g>
|
||||
<path
|
||||
d="M23.227 18.008H20.5869C20.1405 18.008 19.7365 17.7447 19.5553 17.3347L18.4511 14.8444C18.2574 14.4063 18.3641 13.9006 18.7028 13.5623C19.4724 12.7923 19.9188 11.6954 19.8142 10.4966C19.6526 8.6318 18.1352 7.13848 16.2771 7.00945C14.0429 6.85439 12.1827 8.62868 12.1827 10.8411C12.1827 11.9265 12.6312 12.9057 13.351 13.604C13.6794 13.9224 13.7912 14.4032 13.6286 14.8309L12.6975 17.2785C12.6157 17.4931 12.471 17.6778 12.2826 17.808C12.0941 17.9383 11.8708 18.008 11.642 18.008H8.77399C8.06967 18.008 7.5 18.5813 7.5 19.2879V21.3859C7.5 22.0935 8.07071 22.6658 8.77399 22.6658H23.226C23.9303 22.6658 24.5 22.0925 24.5 21.3859V19.2879C24.501 18.5813 23.9303 18.008 23.227 18.008ZM23.139 25H8.86203C8.54819 25 8.2934 24.744 8.2934 24.4287C8.2934 24.1134 8.54819 23.8574 8.86203 23.8574H23.14C23.4539 23.8574 23.7087 24.1134 23.7087 24.4287C23.7086 24.5038 23.6938 24.5781 23.6652 24.6475C23.6365 24.7168 23.5945 24.7798 23.5416 24.8329C23.4887 24.8859 23.4259 24.928 23.3568 24.9567C23.2878 24.9853 23.2137 25.0001 23.139 25Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_ii_0_1"
|
||||
x="-0.48"
|
||||
y="-0.48"
|
||||
width="32.96"
|
||||
height="32.96"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.48" dy="-0.48" />
|
||||
<feGaussianBlur stdDeviation="0.48" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.760784 0 0 0 0 0.470588 0 0 0 0 0.0627451 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_1" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="-0.48" dy="0.48" />
|
||||
<feGaussianBlur stdDeviation="0.74" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_innerShadow_0_1"
|
||||
result="effect2_innerShadow_0_1"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_ii_0_1"
|
||||
x="-0.48"
|
||||
y="-0.48"
|
||||
width="32.96"
|
||||
height="32.96"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="0.48" dy="-0.48" />
|
||||
<feGaussianBlur stdDeviation="0.48" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.760784 0 0 0 0 0.470588 0 0 0 0 0.0627451 0 0 0 0.15 0"
|
||||
/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_0_1" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dx="-0.48" dy="0.48" />
|
||||
<feGaussianBlur stdDeviation="0.74" />
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="effect1_innerShadow_0_1"
|
||||
result="effect2_innerShadow_0_1"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_0_1"
|
||||
x1="-15"
|
||||
y1="-28"
|
||||
x2="25"
|
||||
y2="30.5"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#FEF5E7" />
|
||||
<stop offset="1" stopColor="#FFB84D" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
205
app/person/dashboard/components/PersonalProjectTable.tsx
Normal file
205
app/person/dashboard/components/PersonalProjectTable.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
"use client"
|
||||
|
||||
import { getPersonProjectDashBoard } from "@/components/label/api/project"
|
||||
import { Project } from "@/components/label/api/project/typing"
|
||||
import { Button, Group, Text, UnstyledButton } from "@mantine/core"
|
||||
|
||||
import { IconRefresh } from "@tabler/icons-react"
|
||||
import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
dayjs.extend(duration)
|
||||
|
||||
function formatDurationSeconds(seconds?: number | null) {
|
||||
const value = seconds ?? 0
|
||||
if (!value) return "-"
|
||||
const durationObj = dayjs.duration(value, "seconds")
|
||||
const hours = durationObj.hours()
|
||||
const minutes = durationObj.minutes()
|
||||
if (hours) return `${hours}小时${minutes}分钟`
|
||||
if (minutes) return `${minutes}分钟`
|
||||
return "-"
|
||||
}
|
||||
|
||||
export default function PersonalProjectTable() {
|
||||
const router = useRouter()
|
||||
const [records, setRecords] = useState<
|
||||
Project.PersonProjectDashboardResponseItem[]
|
||||
>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
|
||||
const totalPages = useMemo(() => {
|
||||
const totalItems = records.length
|
||||
return Math.max(1, Math.ceil(totalItems / pageSize))
|
||||
}, [pageSize, records.length])
|
||||
|
||||
const pageRecords = useMemo(() => {
|
||||
const start = (page - 1) * pageSize
|
||||
return records.slice(start, start + pageSize)
|
||||
}, [page, pageSize, records])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getPersonProjectDashBoard()
|
||||
setRecords(res ?? [])
|
||||
setTotalItems(res?.length ?? 0)
|
||||
setPage(1)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
if (page > totalPages) setPage(totalPages)
|
||||
}, [page, totalPages])
|
||||
|
||||
const [currentRowId, setCurrentRowId] = useState<number>(0)
|
||||
|
||||
const originColumn = useMemo(() => {
|
||||
let column: DataTableColumn<Project.PersonProjectDashboardResponseItem>[] =
|
||||
[
|
||||
{
|
||||
accessor: "project_id",
|
||||
title: "项目ID",
|
||||
width: 150,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return (
|
||||
<UnstyledButton
|
||||
px={6}
|
||||
variant="transparent"
|
||||
onClick={async () => {
|
||||
setCurrentRowId(record.project_id)
|
||||
router.push(`/project/info/detail?id=${record.project_id}`)
|
||||
}}
|
||||
title={record.project_id.toString()}
|
||||
c={"brand"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{record.project_id}
|
||||
</UnstyledButton>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "project_name",
|
||||
title: "项目名称",
|
||||
width: 160,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return record.project_name ?? "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "project_type",
|
||||
title: "所属业务",
|
||||
width: 160,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return (
|
||||
<Text
|
||||
title={record.project_id.toString()}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{record.project_type ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "work_time",
|
||||
title: "任务耗时",
|
||||
width: 140,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return formatDurationSeconds(record.work_time)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "project_label_type",
|
||||
title: "标注类别",
|
||||
width: 160,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return record.project_label_type ?? "-"
|
||||
},
|
||||
},
|
||||
]
|
||||
return column
|
||||
}, [router])
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
h="100%"
|
||||
align="stretch"
|
||||
style={{ flexDirection: "column" }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={700} fz="lg" style={{ marginBottom: 8 }}>
|
||||
参与项目
|
||||
</Text>
|
||||
<Button
|
||||
size={"xs"}
|
||||
fz={"sm"}
|
||||
fw={500}
|
||||
radius="sm"
|
||||
onClick={load}
|
||||
loading={loading}
|
||||
leftSection={<IconRefresh size={16} />}>
|
||||
更新
|
||||
</Button>
|
||||
</Group>
|
||||
<Group
|
||||
gap={0}
|
||||
align="stretch"
|
||||
style={{ flex: 1, minHeight: 0, width: "100%" }}>
|
||||
<DataTable<Project.PersonProjectDashboardResponseItem>
|
||||
width="100%"
|
||||
style={{ width: "100%" }}
|
||||
styles={{
|
||||
header: {
|
||||
color: "var(--mantine-color-grey-text)",
|
||||
backgroundColor: "var(--mantine-datatable-striped-color)",
|
||||
},
|
||||
}}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
pinFirstColumn
|
||||
rowBackgroundColor={(record) => {
|
||||
if (record.project_id === currentRowId)
|
||||
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
|
||||
}}
|
||||
idAccessor="project_id"
|
||||
scrollAreaProps={{ type: "auto" }}
|
||||
records={pageRecords}
|
||||
onRecordsPerPageChange={setPageSize}
|
||||
recordsPerPageOptions={[10, 15, 20]}
|
||||
columns={originColumn}
|
||||
totalRecords={totalItems}
|
||||
recordsPerPage={pageSize}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
noRecordsText="暂无数据"
|
||||
recordsPerPageLabel="当前页数"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
225
app/person/dashboard/components/PersonalTaskTable.tsx
Normal file
225
app/person/dashboard/components/PersonalTaskTable.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
"use client"
|
||||
|
||||
import { getUserTaskList } from "@/components/label/api/task"
|
||||
import { Task } from "@/components/label/api/task/typing"
|
||||
import {
|
||||
useBackUrlStore,
|
||||
usePermissionStore,
|
||||
} from "@/components/label/store/auth"
|
||||
import { TaskStatusEnum } from "@/components/label/utils/constants"
|
||||
import { Badge, Button, Group, Text, UnstyledButton } from "@mantine/core"
|
||||
import { IconRefresh } from "@tabler/icons-react"
|
||||
import { DataTable, DataTableColumn } from "mantine-datatable"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
function getStatusColor(status?: number | null) {
|
||||
switch (status) {
|
||||
case 2:
|
||||
return "blue"
|
||||
case 4:
|
||||
return "yellow"
|
||||
case 6:
|
||||
return "orange"
|
||||
case 7:
|
||||
return "green"
|
||||
case 8:
|
||||
return "red"
|
||||
default:
|
||||
return "gray"
|
||||
}
|
||||
}
|
||||
|
||||
export default function PersonalTaskTable() {
|
||||
const user_name = usePermissionStore((s) => s.user_name)
|
||||
const setBackProps = useBackUrlStore((s) => s.setBackProps)
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [records, setRecords] = useState<Task.SimpleTaskItem[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const totalPages = useMemo(() => {
|
||||
return Math.max(1, Math.ceil(totalItems / pageSize))
|
||||
}, [pageSize, totalItems])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!user_name) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getUserTaskList({
|
||||
page_number: page,
|
||||
page_size: pageSize,
|
||||
})
|
||||
setRecords(res?.simple_task_list ?? [])
|
||||
setTotalItems(res?.total_items ?? 0)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, pageSize, user_name])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
useEffect(() => {
|
||||
if (page > totalPages) setPage(totalPages)
|
||||
}, [page, totalPages])
|
||||
|
||||
const resetData = () => {
|
||||
if (page === 1) load()
|
||||
else setPage(1)
|
||||
}
|
||||
|
||||
const [currentRowId, setCurrentRowId] = useState<number>(0)
|
||||
|
||||
const originColumn = useMemo(() => {
|
||||
let column: DataTableColumn<Task.SimpleTaskItem>[] = [
|
||||
{
|
||||
accessor: "project_name",
|
||||
title: "项目名称",
|
||||
width: 150,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return <Text>{record.project_name}</Text>
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "id",
|
||||
title: "任务ID",
|
||||
width: 150,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return (
|
||||
<UnstyledButton
|
||||
px={6}
|
||||
variant="transparent"
|
||||
onClick={async () => {
|
||||
setCurrentRowId(record.id)
|
||||
if ([4, 5, 6, 7].includes(record.label_type)) {
|
||||
setBackProps(`/person/dashboard`)
|
||||
if ([4, 5, 6].includes(record.label_type)) {
|
||||
// router.replace(
|
||||
// `/image/label?project_id=${record.project_id}&task_id=${record.id}`
|
||||
// )
|
||||
window.location.href = `/image/label?project_id=${record.project_id}&task_id=${record.id}`
|
||||
} else if ([7].includes(record.label_type)) {
|
||||
// router.replace(
|
||||
// `/video/label?project_id=${record.project_id}&task_id=${record.id}`
|
||||
// )
|
||||
window.location.href = `/video/label?project_id=${record.project_id}&task_id=${record.id}`
|
||||
}
|
||||
} else {
|
||||
// await runCommand(
|
||||
// record.id.toString(),
|
||||
// user_name,
|
||||
// user_password
|
||||
// )
|
||||
}
|
||||
}}
|
||||
title={record.id.toString()}
|
||||
c={"brand"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{record.id}
|
||||
</UnstyledButton>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "label_status",
|
||||
title: "状态",
|
||||
width: 80,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return (
|
||||
<Badge
|
||||
color={getStatusColor(record.label_status)}
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
padding: "2px 6px",
|
||||
}}>
|
||||
{TaskStatusEnum.get(record.label_status)}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessor: "label_username",
|
||||
title: "标注员",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
accessor: "review1_username",
|
||||
title: "审核员",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
accessor: "review2_username",
|
||||
title: "复审员",
|
||||
width: 80,
|
||||
},
|
||||
]
|
||||
return column
|
||||
}, [setBackProps])
|
||||
|
||||
return (
|
||||
<Group
|
||||
gap="sm"
|
||||
h="100%"
|
||||
align="stretch"
|
||||
style={{ flexDirection: "column" }}>
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={700} fz="lg" style={{ marginBottom: 8 }}>
|
||||
我的任务
|
||||
</Text>
|
||||
<Button
|
||||
size={"xs"}
|
||||
fz={"sm"}
|
||||
fw={500}
|
||||
radius="sm"
|
||||
onClick={resetData}
|
||||
loading={loading}
|
||||
leftSection={<IconRefresh size={16} />}>
|
||||
更新
|
||||
</Button>
|
||||
</Group>
|
||||
<Group
|
||||
gap={0}
|
||||
align="stretch"
|
||||
style={{ flex: 1, minHeight: 0, width: "100%" }}>
|
||||
<DataTable<Task.SimpleTaskItem>
|
||||
width="100%"
|
||||
style={{ width: "100%" }}
|
||||
styles={{
|
||||
header: {
|
||||
color: "var(--mantine-color-grey-text)",
|
||||
backgroundColor: "var(--mantine-datatable-striped-color)",
|
||||
},
|
||||
}}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
pinFirstColumn
|
||||
pinLastColumn
|
||||
rowBackgroundColor={({ id }) => {
|
||||
if (id === currentRowId)
|
||||
return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))"
|
||||
}}
|
||||
scrollAreaProps={{ type: "auto" }}
|
||||
records={records}
|
||||
onRecordsPerPageChange={setPageSize}
|
||||
recordsPerPageOptions={[10, 15, 20]}
|
||||
columns={originColumn}
|
||||
totalRecords={totalItems}
|
||||
recordsPerPage={pageSize}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
noRecordsText="暂无数据"
|
||||
recordsPerPageLabel="当前页数"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,285 @@
|
||||
"use client"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return <>Person Dashboard</>
|
||||
import { getCurrentPersonData } from "@/components/label/api/project"
|
||||
import {
|
||||
BackgroundImage,
|
||||
Center,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core"
|
||||
import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import { JSX, useEffect, useMemo, useState } from "react"
|
||||
import { DataIcon, HourIcon, ReviewIcon } from "./components/Icon"
|
||||
import PersonalProjectTable from "./components/PersonalProjectTable"
|
||||
import PersonalTaskTable from "./components/PersonalTaskTable"
|
||||
import DataImage from "./resource/images/data.png"
|
||||
import DataLogoImage from "./resource/images/data_logo.png"
|
||||
import HourImage from "./resource/images/hour.png"
|
||||
import HourLogoImage from "./resource/images/hour_logo.png"
|
||||
import ReviewImage from "./resource/images/review.png"
|
||||
import ReviewLogoImage from "./resource/images/review_logo.png"
|
||||
|
||||
dayjs.extend(duration)
|
||||
|
||||
function monthText(month: string) {
|
||||
const list = [
|
||||
"一月",
|
||||
"二月",
|
||||
"三月",
|
||||
"四月",
|
||||
"五月",
|
||||
"六月",
|
||||
"七月",
|
||||
"八月",
|
||||
"九月",
|
||||
"十月",
|
||||
"十一月",
|
||||
"十二月",
|
||||
]
|
||||
const idx = Number(month) - 1
|
||||
return list[idx] ?? ""
|
||||
}
|
||||
|
||||
function formatDurationSeconds(seconds?: number | null) {
|
||||
const value = seconds ?? 0
|
||||
if (!value) return "-"
|
||||
const durationObj = dayjs.duration(value, "seconds")
|
||||
const hours = durationObj.hours()
|
||||
const minutes = durationObj.minutes()
|
||||
if (hours) return `${hours}小时${minutes}分钟`
|
||||
if (minutes) return `${minutes}分钟`
|
||||
return "-"
|
||||
}
|
||||
|
||||
function StatCard(props: {
|
||||
title: string
|
||||
timeText: string
|
||||
sizeTitle: string
|
||||
sizeValue: number
|
||||
sizeColor: string
|
||||
background: string
|
||||
backgroundLogo: string
|
||||
Icon: () => JSX.Element
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
timeText,
|
||||
sizeTitle,
|
||||
sizeValue,
|
||||
sizeColor,
|
||||
background,
|
||||
backgroundLogo,
|
||||
Icon,
|
||||
} = props
|
||||
return (
|
||||
<Paper
|
||||
shadow="xs"
|
||||
p="md"
|
||||
withBorder
|
||||
radius={"md"}
|
||||
pos="relative"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
}}>
|
||||
<BackgroundImage
|
||||
src={background}
|
||||
w="100%"
|
||||
h="100%"
|
||||
style={{ position: "absolute", top: 0, left: 0, zIndex: 0 }}
|
||||
/>
|
||||
<BackgroundImage
|
||||
src={backgroundLogo}
|
||||
w={108}
|
||||
h={108}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 8,
|
||||
zIndex: 0,
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
<Group gap="md" align="flex-start" style={{ flex: 1, zIndex: 1 }}>
|
||||
<div style={{ width: 32 }}>
|
||||
<Icon />
|
||||
</div>
|
||||
<Stack gap={6} style={{ flex: 1 }}>
|
||||
<div>
|
||||
<Text fw={700} fz="lg" lh={1.4}>
|
||||
{title}
|
||||
</Text>
|
||||
<Title order={3} style={{ marginTop: 6 }}>
|
||||
{timeText}
|
||||
</Title>
|
||||
</div>
|
||||
<Stack gap={6}>
|
||||
<Text fw={700} fz="lg">
|
||||
{sizeTitle}
|
||||
</Text>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
px={8}
|
||||
py={4}
|
||||
radius="sm"
|
||||
style={{
|
||||
background: "#fff",
|
||||
width: "fit-content",
|
||||
color: sizeColor,
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
}}>
|
||||
{sizeValue}
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PersonDashboardPage() {
|
||||
const [data, setData] = useState<{
|
||||
label_data_size: number
|
||||
label_work_time: number
|
||||
review1_data_size: number
|
||||
review1_work_time: number
|
||||
review2_data_size: number
|
||||
review2_work_time: number
|
||||
} | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const res = await getCurrentPersonData({
|
||||
date: dayjs().format("YYYY-MM-DD"),
|
||||
})
|
||||
setData(res)
|
||||
}
|
||||
run()
|
||||
}, [])
|
||||
|
||||
const dataText = useMemo(() => {
|
||||
return {
|
||||
label: formatDurationSeconds(data?.label_work_time ?? 0),
|
||||
review1: formatDurationSeconds(data?.review1_work_time ?? 0),
|
||||
review2: formatDurationSeconds(data?.review2_work_time ?? 0),
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
||||
<Group align="stretch" gap="md" wrap="nowrap" h={180}>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
width: 160,
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}>
|
||||
<Text fz="lg" fw={700}>
|
||||
今日
|
||||
</Text>
|
||||
<Center>
|
||||
<Paper
|
||||
radius="md"
|
||||
p="sm"
|
||||
style={{
|
||||
width: 72,
|
||||
height: 72,
|
||||
background: "rgba(0, 161, 255, 0.08)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 4,
|
||||
}}>
|
||||
<Text fz={24} fw={700} lh={1}>
|
||||
{dayjs().format("DD")}
|
||||
</Text>
|
||||
<Text fz={16}>{monthText(dayjs().format("MM"))}</Text>
|
||||
</Paper>
|
||||
</Center>
|
||||
</Paper>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" style={{ flex: 1 }}>
|
||||
<StatCard
|
||||
title="标注工时"
|
||||
timeText={dataText.label}
|
||||
sizeTitle="标注数据量"
|
||||
sizeValue={data?.label_data_size ?? 0}
|
||||
sizeColor="#E03131"
|
||||
background={DataImage.src}
|
||||
backgroundLogo={DataLogoImage.src}
|
||||
Icon={DataIcon}
|
||||
/>
|
||||
<StatCard
|
||||
title="审核工时"
|
||||
timeText={dataText.review1}
|
||||
sizeTitle="审核数据量"
|
||||
sizeValue={data?.review1_data_size ?? 0}
|
||||
sizeColor="#2F9E44"
|
||||
background={HourImage.src}
|
||||
backgroundLogo={HourLogoImage.src}
|
||||
Icon={HourIcon}
|
||||
/>
|
||||
<StatCard
|
||||
title="复审工时"
|
||||
timeText={dataText.review2}
|
||||
sizeTitle="复审数据量"
|
||||
sizeValue={data?.review2_data_size ?? 0}
|
||||
sizeColor="#F08C00"
|
||||
background={ReviewImage.src}
|
||||
backgroundLogo={ReviewLogoImage.src}
|
||||
Icon={ReviewIcon}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md" style={{ flex: 1 }}>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<PersonalTaskTable />
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
shadow="xs"
|
||||
withBorder
|
||||
p="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<PersonalProjectTable />
|
||||
</div>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
BIN
app/person/dashboard/resource/images/data.png
Normal file
BIN
app/person/dashboard/resource/images/data.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
app/person/dashboard/resource/images/data_logo.png
Normal file
BIN
app/person/dashboard/resource/images/data_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
BIN
app/person/dashboard/resource/images/hour.png
Normal file
BIN
app/person/dashboard/resource/images/hour.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
app/person/dashboard/resource/images/hour_logo.png
Normal file
BIN
app/person/dashboard/resource/images/hour_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
BIN
app/person/dashboard/resource/images/review.png
Normal file
BIN
app/person/dashboard/resource/images/review.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
app/person/dashboard/resource/images/review_logo.png
Normal file
BIN
app/person/dashboard/resource/images/review_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
541
app/person/report/components/DailyReportModal.tsx
Normal file
541
app/person/report/components/DailyReportModal.tsx
Normal file
@@ -0,0 +1,541 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
addDailyWorkData,
|
||||
editDailyWorkData,
|
||||
} from "@/components/label/api/daily"
|
||||
import { Daily } from "@/components/label/api/daily/typing"
|
||||
import {
|
||||
useAllUserStore,
|
||||
usePermissionStore,
|
||||
} from "@/components/label/store/auth"
|
||||
import {
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { notifications } from "@mantine/notifications"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import {
|
||||
accountTypeOpts,
|
||||
actualTypeOpts,
|
||||
objTypeOpts,
|
||||
performanceOpts,
|
||||
setTypeOpts,
|
||||
workTypeOpts,
|
||||
} from "../config"
|
||||
|
||||
function normalizeTimeValue(value?: string | null) {
|
||||
if (!value) return ""
|
||||
return value.length >= 5 ? value.slice(0, 5) : value
|
||||
}
|
||||
|
||||
function toTimeWithSeconds(value?: string) {
|
||||
if (!value) return undefined
|
||||
return value.length === 5 ? `${value}:00` : value
|
||||
}
|
||||
|
||||
type LeaderTreeNode = {
|
||||
title?: string
|
||||
value?: string | number
|
||||
children?: LeaderTreeNode[]
|
||||
}
|
||||
|
||||
function flattenLeaderTree(
|
||||
nodes: LeaderTreeNode[],
|
||||
prefixTitle: string[] = []
|
||||
): Array<{ label: string; value: string }> {
|
||||
const result: Array<{ label: string; value: string }> = []
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const title = node.title ?? ""
|
||||
const nextPrefix = title ? [...prefixTitle, title] : prefixTitle
|
||||
const children = node.children ?? []
|
||||
const hasChildren = children.length > 0
|
||||
const valueRaw = node.value
|
||||
|
||||
if (hasChildren) {
|
||||
result.push(...flattenLeaderTree(children, nextPrefix))
|
||||
return
|
||||
}
|
||||
|
||||
if (valueRaw === undefined || valueRaw === null) return
|
||||
if (typeof valueRaw === "string" && valueRaw.includes("-")) return
|
||||
|
||||
result.push({
|
||||
label: nextPrefix.join(" / "),
|
||||
value: String(valueRaw),
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function HourRightSection() {
|
||||
return (
|
||||
<Text size="xs" c="dimmed">
|
||||
小时(H)
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DailyReportModal(props: {
|
||||
opened: boolean
|
||||
record: Daily.DailyWorkList | null
|
||||
projectOptions: Array<{ label: string; value: string }>
|
||||
onCloseAction: (refresh?: boolean) => void
|
||||
}) {
|
||||
const { opened, record, onCloseAction, projectOptions } = props
|
||||
|
||||
const user_id = usePermissionStore((s) => s.user_id)
|
||||
const user_name = usePermissionStore((s) => s.user_name)
|
||||
const treeData = useAllUserStore((s) => s.treeData)
|
||||
|
||||
const leaderOptions = useMemo(() => {
|
||||
return flattenLeaderTree(treeData ?? [])
|
||||
}, [treeData])
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
user_name: user_name ?? "",
|
||||
date: "",
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
set_type: "",
|
||||
actual_type: "",
|
||||
project_id: "",
|
||||
leader_uid: "",
|
||||
work_type: "",
|
||||
accounting_type: "",
|
||||
performance_accounting: "",
|
||||
obj_type: "",
|
||||
work_time: undefined as number | undefined,
|
||||
rate: undefined as number | undefined,
|
||||
standard_size: undefined as number | undefined,
|
||||
completed_size: undefined as number | undefined,
|
||||
residual_size: undefined as number | undefined,
|
||||
residual_work_time: undefined as number | undefined,
|
||||
residual_pm: "",
|
||||
remarks: "",
|
||||
},
|
||||
validate: {
|
||||
date: (v) => (v ? null : "请输入日期"),
|
||||
// set_type: (v) => (v ? null : "请选择规定类型"),
|
||||
actual_type: (v) => (v ? null : "请选择出勤类型"),
|
||||
},
|
||||
validateInputOnChange: true,
|
||||
validateInputOnBlur: true,
|
||||
clearInputErrorOnChange: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) return
|
||||
|
||||
if (!record) {
|
||||
form.setValues({
|
||||
user_name: user_name ?? "",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
set_type: "",
|
||||
actual_type: "",
|
||||
project_id: "",
|
||||
leader_uid: "",
|
||||
work_type: "",
|
||||
accounting_type: "",
|
||||
performance_accounting: "",
|
||||
obj_type: "",
|
||||
work_time: undefined,
|
||||
rate: undefined,
|
||||
standard_size: undefined,
|
||||
completed_size: undefined,
|
||||
residual_size: undefined,
|
||||
residual_work_time: undefined,
|
||||
residual_pm: "",
|
||||
remarks: "",
|
||||
})
|
||||
form.resetDirty()
|
||||
return
|
||||
}
|
||||
|
||||
form.setValues({
|
||||
user_name: user_name ?? "",
|
||||
date: record.date ?? "",
|
||||
start_time: normalizeTimeValue(record.start_time),
|
||||
end_time: normalizeTimeValue(record.end_time),
|
||||
set_type: record.set_type ?? "",
|
||||
actual_type: record.actual_type ?? "",
|
||||
project_id:
|
||||
record.project_id === undefined || record.project_id === null
|
||||
? ""
|
||||
: String(record.project_id),
|
||||
leader_uid:
|
||||
record.leader_uid === undefined || record.leader_uid === null
|
||||
? ""
|
||||
: String(record.leader_uid),
|
||||
work_type: record.work_type ?? "",
|
||||
accounting_type: record.accounting_type ?? "",
|
||||
performance_accounting:
|
||||
record.performance_accounting === true
|
||||
? "true"
|
||||
: record.performance_accounting === false
|
||||
? "false"
|
||||
: "",
|
||||
obj_type:
|
||||
record.obj_type === undefined || record.obj_type === null
|
||||
? ""
|
||||
: String(record.obj_type),
|
||||
work_time: record.work_time ?? undefined,
|
||||
rate: record.rate ?? undefined,
|
||||
standard_size: record.standard_size ?? undefined,
|
||||
completed_size: record.completed_size ?? undefined,
|
||||
residual_size: record.residual_size ?? undefined,
|
||||
residual_work_time: record.residual_work_time ?? undefined,
|
||||
residual_pm:
|
||||
record.residual_pm === true
|
||||
? "正"
|
||||
: record.residual_pm === false
|
||||
? "负"
|
||||
: "",
|
||||
remarks: record.remarks ?? "",
|
||||
})
|
||||
form.resetDirty()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [opened, record, user_name])
|
||||
|
||||
useEffect(() => {
|
||||
const workTime = form.values.work_time
|
||||
const rate = form.values.rate
|
||||
if (workTime !== undefined && rate !== undefined) {
|
||||
form.setFieldValue("standard_size", Number((workTime * rate).toFixed(1)))
|
||||
} else {
|
||||
form.setFieldValue("standard_size", undefined)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const standardSize = form.values.standard_size
|
||||
const completedSize = form.values.completed_size
|
||||
const workTime = form.values.work_time
|
||||
|
||||
if (
|
||||
standardSize !== undefined &&
|
||||
completedSize !== undefined &&
|
||||
workTime !== undefined &&
|
||||
standardSize !== 0
|
||||
) {
|
||||
const residualSize = completedSize - standardSize
|
||||
const residualWorkTime = Number(
|
||||
((residualSize / standardSize) * workTime).toFixed(1)
|
||||
)
|
||||
form.setFieldValue("residual_size", Number(residualSize.toFixed(1)))
|
||||
form.setFieldValue("residual_work_time", residualWorkTime)
|
||||
form.setFieldValue("residual_pm", residualSize >= 0 ? "正" : "负")
|
||||
} else {
|
||||
form.setFieldValue("residual_size", undefined)
|
||||
form.setFieldValue("residual_work_time", undefined)
|
||||
form.setFieldValue("residual_pm", "")
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleSubmit = form.onSubmit(async (values) => {
|
||||
const uid = typeof user_id === "number" ? user_id : Number(user_id ?? 0)
|
||||
if (!uid) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: "提交失败",
|
||||
message: "未获取到用户信息",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const params: Daily.CreateProps = {
|
||||
uid,
|
||||
date: values.date,
|
||||
start_time: toTimeWithSeconds(values.start_time),
|
||||
end_time: toTimeWithSeconds(values.end_time),
|
||||
set_type: values.set_type,
|
||||
actual_type: values.actual_type,
|
||||
project_id: values.project_id ? Number(values.project_id) : undefined,
|
||||
leader_uid: values.leader_uid ? Number(values.leader_uid) : undefined,
|
||||
work_type: values.work_type || undefined,
|
||||
accounting_type: values.accounting_type || undefined,
|
||||
performance_accounting:
|
||||
values.performance_accounting === "true"
|
||||
? true
|
||||
: values.performance_accounting === "false"
|
||||
? false
|
||||
: undefined,
|
||||
obj_type: values.obj_type ? Number(values.obj_type) : undefined,
|
||||
work_time: values.work_time,
|
||||
rate: values.rate,
|
||||
standard_size: values.standard_size,
|
||||
completed_size: values.completed_size,
|
||||
residual_size: values.residual_size,
|
||||
residual_work_time: values.residual_work_time,
|
||||
residual_pm:
|
||||
values.residual_pm === "正"
|
||||
? true
|
||||
: values.residual_pm === "负"
|
||||
? false
|
||||
: undefined,
|
||||
remarks: values.remarks || undefined,
|
||||
}
|
||||
|
||||
try {
|
||||
if (record?.id) {
|
||||
await editDailyWorkData(params, record.id)
|
||||
notifications.show({
|
||||
color: "green",
|
||||
title: "操作成功",
|
||||
message: "已更新日报",
|
||||
})
|
||||
} else {
|
||||
await addDailyWorkData(params)
|
||||
notifications.show({
|
||||
color: "green",
|
||||
title: "操作成功",
|
||||
message: "已提交日报",
|
||||
})
|
||||
}
|
||||
onCloseAction(true)
|
||||
form.reset()
|
||||
} catch (e) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
title: "操作失败",
|
||||
message: e instanceof Error ? e.message : "请求失败",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
form.reset()
|
||||
onCloseAction()
|
||||
}}
|
||||
title={record ? "编辑日报" : "填写日报"}
|
||||
size={840}
|
||||
centered
|
||||
closeOnClickOutside={false}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm">
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
|
||||
<TextInput
|
||||
label="姓名"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.user_name}
|
||||
readOnly
|
||||
/>
|
||||
<TextInput
|
||||
label="日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
withAsterisk
|
||||
{...form.getInputProps("date")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<TextInput
|
||||
label="开始时间"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="time"
|
||||
{...form.getInputProps("start_time")}
|
||||
/>
|
||||
<TextInput
|
||||
label="结束时间"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="time"
|
||||
{...form.getInputProps("end_time")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<Select
|
||||
label="规定类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
data={setTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("set_type")}
|
||||
/>
|
||||
<Select
|
||||
label="出勤类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
data={actualTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("actual_type")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<Select
|
||||
label="项目名称"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={projectOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("project_id")}
|
||||
/>
|
||||
<Select
|
||||
label="任务组长"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={leaderOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("leader_uid")}
|
||||
/>
|
||||
<Select
|
||||
label="工作类别"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={workTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("work_type")}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="对象类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={objTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("obj_type")}
|
||||
/>
|
||||
<Select
|
||||
label="绩效核算"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={performanceOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("performance_accounting")}
|
||||
/>
|
||||
<Select
|
||||
label="核算方式"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={accountTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("accounting_type")}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="工时"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
{...form.getInputProps("work_time")}
|
||||
/>
|
||||
<NumberInput
|
||||
label="额定倍速"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("rate")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<NumberInput
|
||||
label="标准量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.standard_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="完成量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("completed_size")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<NumberInput
|
||||
label="余量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="余量工时"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_work_time}
|
||||
readOnly
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
/>
|
||||
<TextInput
|
||||
label="余量正负"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_pm}
|
||||
readOnly
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="备注"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
{...form.getInputProps("remarks")}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
onClick={() => {
|
||||
form.reset()
|
||||
onCloseAction()
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="xs" radius="xs" type="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
68
app/person/report/config.ts
Normal file
68
app/person/report/config.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export const setTypeOpts = [
|
||||
{ label: "出勤", value: "出勤" },
|
||||
{ label: "周末休", value: "周末休" },
|
||||
{ label: "节假休", value: "节假休" },
|
||||
]
|
||||
|
||||
export const actualTypeOpts = [
|
||||
{ label: "出勤", value: "出勤" },
|
||||
{ label: "周末休", value: "周末休" },
|
||||
{ label: "节假休", value: "节假休" },
|
||||
{ label: "调休", value: "调休" },
|
||||
{ label: "补勤", value: "补勤" },
|
||||
{ label: "增勤", value: "增勤" },
|
||||
{ label: "年假", value: "年假" },
|
||||
{ label: "事假", value: "事假" },
|
||||
{ label: "病假", value: "病假" },
|
||||
{ label: "婚假", value: "婚假" },
|
||||
{ label: "产假", value: "产假" },
|
||||
{ label: "陪产假", value: "陪产假" },
|
||||
{ label: "丧假", value: "丧假" },
|
||||
{ label: "其他", value: "其他" },
|
||||
]
|
||||
|
||||
export const workTypeOpts = [
|
||||
{ label: "标注", value: "标注" },
|
||||
{ label: "审核", value: "审核" },
|
||||
{ label: "复审", value: "复审" },
|
||||
{ label: "标注返工", value: "标注返工" },
|
||||
{ label: "审核返工", value: "审核返工" },
|
||||
{ label: "复审返工", value: "复审返工" },
|
||||
{ label: "管理", value: "管理" },
|
||||
]
|
||||
|
||||
export const accountTypeOpts = [
|
||||
{ label: "对象核算", value: "对象核算" },
|
||||
{ label: "素材核算", value: "素材核算" },
|
||||
{ label: "会议", value: "会议" },
|
||||
{ label: "培训", value: "培训" },
|
||||
{ label: "项目管理", value: "项目管理" },
|
||||
{ label: "出差", value: "出差" },
|
||||
{ label: "断电", value: "断电" },
|
||||
{ label: "网络故障", value: "网络故障" },
|
||||
{ label: "平台故障", value: "平台故障" },
|
||||
{ label: "支援行政", value: "支援行政" },
|
||||
{ label: "公司活动", value: "公司活动" },
|
||||
{ label: "请假", value: "请假" },
|
||||
{ label: "其他", value: "其他" },
|
||||
]
|
||||
|
||||
export const performanceOpts = [
|
||||
{ label: "是", value: "true" },
|
||||
{ label: "否", value: "false" },
|
||||
]
|
||||
|
||||
export const objTypeOpts = [
|
||||
{ label: "静态", value: "0" },
|
||||
{ label: "动态", value: "1" },
|
||||
]
|
||||
|
||||
export const performanceTextMap = new Map<boolean, string>([
|
||||
[true, "是"],
|
||||
[false, "否"],
|
||||
])
|
||||
|
||||
export const objTypeTextMap = new Map<number, string>([
|
||||
[0, "静态"],
|
||||
[1, "动态"],
|
||||
])
|
||||
@@ -1,5 +1,710 @@
|
||||
"use client"
|
||||
|
||||
export default function ReportPage() {
|
||||
return <>Person Report</>
|
||||
import {
|
||||
deleteDailyWorkData,
|
||||
getSelfDailyWorkList,
|
||||
} from "@/components/label/api/daily"
|
||||
import { Daily } from "@/components/label/api/daily/typing"
|
||||
import { getSelfProjectList } from "@/components/label/api/project"
|
||||
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,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
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 DailyReportModal from "./components/DailyReportModal"
|
||||
import {
|
||||
accountTypeOpts,
|
||||
actualTypeOpts,
|
||||
objTypeOpts,
|
||||
objTypeTextMap,
|
||||
performanceOpts,
|
||||
performanceTextMap,
|
||||
setTypeOpts,
|
||||
workTypeOpts,
|
||||
} from "./config"
|
||||
|
||||
type LeaderTreeNode = {
|
||||
title?: string
|
||||
value?: string | number
|
||||
children?: LeaderTreeNode[]
|
||||
}
|
||||
|
||||
function flattenLeaderTree(
|
||||
nodes: LeaderTreeNode[],
|
||||
prefixTitle: string[] = []
|
||||
): Array<{ label: string; value: string }> {
|
||||
const result: Array<{ label: string; value: string }> = []
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const title = node.title ?? ""
|
||||
const nextPrefix = title ? [...prefixTitle, title] : prefixTitle
|
||||
const children = node.children ?? []
|
||||
const hasChildren = children.length > 0
|
||||
const valueRaw = node.value
|
||||
|
||||
if (hasChildren) {
|
||||
result.push(...flattenLeaderTree(children, nextPrefix))
|
||||
return
|
||||
}
|
||||
|
||||
if (valueRaw === undefined || valueRaw === null) return
|
||||
if (typeof valueRaw === "string" && valueRaw.includes("-")) return
|
||||
|
||||
result.push({
|
||||
label: nextPrefix.join(" / "),
|
||||
value: String(valueRaw),
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function weekDayText(day: number) {
|
||||
const list = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
||||
return list[day] ?? "-"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export default function PersonReportPage() {
|
||||
const treeData = useAllUserStore((s) => s.treeData)
|
||||
|
||||
const leaderOptions = useMemo(() => {
|
||||
return flattenLeaderTree(treeData ?? [])
|
||||
}, [treeData])
|
||||
|
||||
const defaultFilters = useMemo(() => {
|
||||
return {
|
||||
date_start: dayjs().subtract(6, "day").format("YYYY-MM-DD"),
|
||||
date_end: dayjs().format("YYYY-MM-DD"),
|
||||
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<Daily.DailyWorkList[]>([])
|
||||
const [summary, setSummary] = useState<Daily.Summary | null>(null)
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: "id",
|
||||
direction: "desc",
|
||||
})
|
||||
|
||||
const [modalOpened, setModalOpened] = useState(false)
|
||||
const [editingRecord, setEditingRecord] =
|
||||
useState<Daily.DailyWorkList | null>(null)
|
||||
|
||||
const [searchExpanded, setSearchExpanded] = useState(false)
|
||||
|
||||
const queryParams = useMemo(() => {
|
||||
const params: Daily.Request = {
|
||||
page_number: page,
|
||||
page_size: pageSize,
|
||||
flag: 0,
|
||||
date_start: appliedFilters.date_start || undefined,
|
||||
date_end: appliedFilters.date_end || undefined,
|
||||
set_type: appliedFilters.set_type || undefined,
|
||||
actual_type: appliedFilters.actual_type || undefined,
|
||||
leader_uid: appliedFilters.leader_uid.length
|
||||
? appliedFilters.leader_uid
|
||||
.map((v) => Number(v))
|
||||
.filter((v) => Number.isFinite(v))
|
||||
: undefined,
|
||||
work_type: appliedFilters.work_type || undefined,
|
||||
accounting_type: appliedFilters.accounting_type || undefined,
|
||||
project_id: appliedFilters.project_id.length
|
||||
? appliedFilters.project_id
|
||||
.map((v) => Number(v))
|
||||
.filter((v) => Number.isFinite(v))
|
||||
: undefined,
|
||||
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 getSelfDailyWorkList(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)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cols: DataTableColumn<Daily.DailyWorkList>[] = [
|
||||
{ accessor: "id", title: "ID", width: 80, sortable: true },
|
||||
{
|
||||
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) => record.set_type ?? "-",
|
||||
},
|
||||
{
|
||||
accessor: "actual_type",
|
||||
title: "实际类型",
|
||||
width: 120,
|
||||
render: (record) => record.actual_type ?? "-",
|
||||
},
|
||||
{ accessor: "leader_name", title: "任务组长", width: 120 },
|
||||
{ accessor: "work_type", title: "工作类别", width: 140 },
|
||||
{ accessor: "accounting_type", title: "核算方式", width: 140 },
|
||||
{
|
||||
accessor: "project_name",
|
||||
title: "项目名称",
|
||||
width: 220,
|
||||
render: (record) => (
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
maxWidth: 220,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{record.project_name ?? "-"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "performance_accounting",
|
||||
title: "绩效核算",
|
||||
width: 110,
|
||||
render: (record) => {
|
||||
if (record.performance_accounting === true)
|
||||
return <Badge color="green">{performanceTextMap.get(true)}</Badge>
|
||||
if (record.performance_accounting === false)
|
||||
return <Badge color="gray">{performanceTextMap.get(false)}</Badge>
|
||||
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: "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) => (
|
||||
<Text
|
||||
size="sm"
|
||||
style={{
|
||||
maxWidth: 200,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{record.remarks ?? "-"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "operation",
|
||||
title: "操作",
|
||||
width: 90,
|
||||
textAlign: "center",
|
||||
render: (record) => {
|
||||
const disabled = !!record.lock_status
|
||||
return (
|
||||
<Group gap={6} justify="center" wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
setEditingRecord(record)
|
||||
setModalOpened(true)
|
||||
}}>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="red"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled || !record.id) return
|
||||
modals.openConfirmModal({
|
||||
title: "删除日报",
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
删除后不可恢复,你确定删除该日报吗?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "删除", cancel: "取消" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: async () => {
|
||||
await deleteDailyWorkData(record.id as number)
|
||||
notifications.show({
|
||||
color: "green",
|
||||
title: "操作成功",
|
||||
message: "已删除日报",
|
||||
})
|
||||
load()
|
||||
},
|
||||
})
|
||||
}}>
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
return cols
|
||||
}, [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])
|
||||
|
||||
const asyncGetSelfProjectList = useCallback(async () => {
|
||||
const res = await getSelfProjectList()
|
||||
setProjectOptions(
|
||||
(res ?? []).map((r) => ({ label: r.label, value: String(r.value) }))
|
||||
)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => asyncGetSelfProjectList())
|
||||
}, [asyncGetSelfProjectList])
|
||||
|
||||
return (
|
||||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
}}>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<Text fw={700}>搜索条件</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="transparent"
|
||||
rightSection={
|
||||
searchExpanded ? (
|
||||
<IconChevronUp size={16} />
|
||||
) : (
|
||||
<IconChevronDown size={16} />
|
||||
)
|
||||
}
|
||||
onClick={() => setSearchExpanded((v) => !v)}>
|
||||
{searchExpanded ? "收起" : "展开"}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={() => {
|
||||
setFilters(defaultFilters)
|
||||
setAppliedFilters(defaultFilters)
|
||||
setPage(1)
|
||||
setQueryTrigger((v) => v + 1)
|
||||
}}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="xs"
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onClick={() => {
|
||||
setAppliedFilters(filters)
|
||||
setPage(1)
|
||||
setQueryTrigger((v) => v + 1)
|
||||
}}>
|
||||
查询
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
<Collapse
|
||||
in={searchExpanded}
|
||||
transitionDuration={150}
|
||||
transitionTimingFunction="ease"
|
||||
animateOpacity>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
|
||||
<TextInput
|
||||
label="开始日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
value={filters.date_start}
|
||||
onChange={(e) =>
|
||||
setFilters((s) => ({
|
||||
...s,
|
||||
date_start: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
label="结束日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
value={filters.date_end}
|
||||
onChange={(e) => {
|
||||
setFilters((s) => ({ ...s, date_end: e.target.value }))
|
||||
}}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="项目名称"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={projectOptions}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.project_id}
|
||||
onChange={(v) => setFilters((s) => ({ ...s, project_id: v }))}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="任务组长"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={leaderOptions}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.leader_uid}
|
||||
onChange={(v) => setFilters((s) => ({ ...s, leader_uid: v }))}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="规定类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={setTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.set_type ? [filters.set_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, set_type: v[0] ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="出勤类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={actualTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.actual_type ? [filters.actual_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, actual_type: v[0] ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="工作类别"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={workTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.work_type ? [filters.work_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, work_type: v[0] ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="核算方式"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={accountTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.accounting_type ? [filters.accounting_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="绩效核算"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={performanceOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={
|
||||
filters.performance_accounting
|
||||
? [filters.performance_accounting]
|
||||
: []
|
||||
}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({
|
||||
...s,
|
||||
performance_accounting: v[0] ?? "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
label="对象类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={objTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.obj_type ? [filters.obj_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, obj_type: v[0] ?? "" }))
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 12,
|
||||
}}>
|
||||
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
|
||||
<Flex justify={"space-between"} align="center" w="100%">
|
||||
<Button
|
||||
leftSection={<IconPlus size={12} />}
|
||||
size="xs"
|
||||
radius={"xs"}
|
||||
onClick={() => {
|
||||
setEditingRecord(null)
|
||||
setModalOpened(true)
|
||||
}}>
|
||||
填写日报
|
||||
</Button>
|
||||
<ActionIcon onClick={load} variant="transparent">
|
||||
<IconRefresh size={16} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<DataTable<Daily.DailyWorkList>
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
<Paper withBorder radius="xs" p="sm">
|
||||
<Group justify="space-between" wrap="wrap">
|
||||
<Text size="sm" fw={700}>
|
||||
当前页总计
|
||||
</Text>
|
||||
<Group gap="md">
|
||||
<Text size="sm">工时:{summaryText.total_work_time}</Text>
|
||||
<Text size="sm">标准量:{summaryText.total_standard_size}</Text>
|
||||
<Text size="sm">
|
||||
完成量:{summaryText.total_completed_size}
|
||||
</Text>
|
||||
<Text size="sm">余量:{summaryText.total_residual_size}</Text>
|
||||
<Text size="sm">
|
||||
余量工时:{summaryText.total_residual_work_time}
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<DailyReportModal
|
||||
opened={modalOpened}
|
||||
record={editingRecord}
|
||||
projectOptions={projectOptions}
|
||||
onCloseAction={(refresh) => {
|
||||
setModalOpened(false)
|
||||
setEditingRecord(null)
|
||||
if (refresh) load()
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user