Files
labelmain/app/person/report/components/DailyReportModal.tsx
2026-04-14 16:10:13 +08:00

516 lines
15 KiB
TypeScript

"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,
Paper,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core"
import { useForm } from "@mantine/form"
import { notifications } from "@mantine/notifications"
import { useEffect, useMemo } from "react"
import classes from "./DailyReportModal.module.css"
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
}, [form.values.work_time, form.values.rate])
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
}, [
form.values.standard_size,
form.values.completed_size,
form.values.work_time,
])
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
classNames={{
content: classes.modalContent,
header: classes.modalHeader,
title: classes.modalTitle,
body: classes.modalBody,
}}
overlayProps={{ backgroundOpacity: 0.22 }}
transitionProps={{
transition: "fade",
duration: 100,
timingFunction: "ease",
}}
closeOnClickOutside={false}>
<form onSubmit={handleSubmit}>
<Paper
withBorder
radius="lg"
p="md"
shadow="xs"
className={classes.formSurface}>
<Stack gap="md" className={classes.formPanel}>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
<TextInput label="姓名" value={form.values.user_name} readOnly />
<TextInput
label="日期"
type="date"
withAsterisk
{...form.getInputProps("date")}
/>
<div />
<TextInput
label="开始时间"
type="time"
{...form.getInputProps("start_time")}
/>
<TextInput
label="结束时间"
type="time"
{...form.getInputProps("end_time")}
/>
<div />
<Select
label="规定类型"
withAsterisk
data={setTypeOpts}
searchable
clearable
{...form.getInputProps("set_type")}
/>
<Select
label="出勤类型"
withAsterisk
data={actualTypeOpts}
searchable
clearable
{...form.getInputProps("actual_type")}
/>
<div />
<Select
label="项目名称"
data={projectOptions}
searchable
clearable
{...form.getInputProps("project_id")}
/>
<Select
label="任务组长"
data={leaderOptions}
searchable
clearable
{...form.getInputProps("leader_uid")}
/>
<Select
label="工作类别"
data={workTypeOpts}
searchable
clearable
{...form.getInputProps("work_type")}
/>
<Select
label="对象类型"
data={objTypeOpts}
searchable
clearable
{...form.getInputProps("obj_type")}
/>
<Select
label="绩效核算"
data={performanceOpts}
searchable
clearable
{...form.getInputProps("performance_accounting")}
/>
<Select
label="核算方式"
data={accountTypeOpts}
searchable
clearable
{...form.getInputProps("accounting_type")}
/>
<NumberInput
label="工时"
min={0}
decimalScale={1}
allowNegative={false}
hideControls
rightSection={<HourRightSection />}
rightSectionWidth={78}
{...form.getInputProps("work_time")}
/>
<NumberInput
label="额定倍速"
min={0}
allowNegative={false}
hideControls
{...form.getInputProps("rate")}
/>
<div />
<NumberInput
label="标准量"
value={form.values.standard_size}
readOnly
hideControls
/>
<NumberInput
label="完成量"
min={0}
allowNegative={false}
hideControls
{...form.getInputProps("completed_size")}
/>
<div />
<NumberInput
label="余量"
value={form.values.residual_size}
readOnly
hideControls
/>
<NumberInput
label="余量工时"
value={form.values.residual_work_time}
readOnly
hideControls
rightSection={<HourRightSection />}
rightSectionWidth={78}
/>
<TextInput
label="余量正负"
value={form.values.residual_pm}
readOnly
/>
</SimpleGrid>
<TextInput label="备注" {...form.getInputProps("remarks")} />
<Group justify="flex-end" gap="sm">
<Button
variant="default"
onClick={() => {
form.reset()
onCloseAction()
}}>
</Button>
<Button type="submit"></Button>
</Group>
</Stack>
</Paper>
</form>
</Modal>
)
}