556 lines
16 KiB
TypeScript
556 lines
16 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 {
|
|
accountTypeOpts,
|
|
actualTypeOpts,
|
|
objTypeOpts,
|
|
performanceOpts,
|
|
setTypeOpts,
|
|
workTypeOpts,
|
|
} from "../config"
|
|
import classes from "./DailyReportModal.module.css"
|
|
|
|
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>
|
|
)
|
|
}
|
|
|
|
function getEmptyFormValues(currentUserName?: string) {
|
|
return {
|
|
user_name: currentUserName ?? "",
|
|
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: "",
|
|
}
|
|
}
|
|
|
|
function getCreateFormValues(currentUserName?: string) {
|
|
return {
|
|
...getEmptyFormValues(currentUserName),
|
|
date: new Date().toISOString().slice(0, 10),
|
|
}
|
|
}
|
|
|
|
function getRecordFormValues(
|
|
record: Daily.DailyWorkList,
|
|
currentUserName?: string
|
|
) {
|
|
return {
|
|
user_name: currentUserName ?? "",
|
|
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 ?? "",
|
|
}
|
|
}
|
|
|
|
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: getEmptyFormValues(user_name),
|
|
validate: {
|
|
date: (v) => (v ? null : "请输入日期"),
|
|
set_type: (v) => (v ? null : "请选择规定类型"),
|
|
actual_type: (v) => (v ? null : "请选择出勤类型"),
|
|
},
|
|
validateInputOnChange: true,
|
|
validateInputOnBlur: true,
|
|
clearInputErrorOnChange: true,
|
|
})
|
|
|
|
const resetFormState = () => {
|
|
form.setValues(getEmptyFormValues(user_name))
|
|
form.clearErrors()
|
|
form.resetDirty()
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!opened) {
|
|
resetFormState()
|
|
return
|
|
}
|
|
|
|
if (!record) {
|
|
form.setValues(getCreateFormValues(user_name))
|
|
form.clearErrors()
|
|
form.resetDirty()
|
|
return
|
|
}
|
|
|
|
form.setValues(getRecordFormValues(record, user_name))
|
|
form.clearErrors()
|
|
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: "已提交日报",
|
|
})
|
|
}
|
|
resetFormState()
|
|
onCloseAction(true)
|
|
} catch (e) {
|
|
notifications.show({
|
|
color: "red",
|
|
title: "操作失败",
|
|
message: e instanceof Error ? e.message : "请求失败",
|
|
})
|
|
}
|
|
})
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={() => {
|
|
resetFormState()
|
|
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")}
|
|
value={form.values.set_type || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("set_type", value ?? "")
|
|
}
|
|
/>
|
|
<Select
|
|
label="出勤类型"
|
|
withAsterisk
|
|
data={actualTypeOpts}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("actual_type")}
|
|
value={form.values.actual_type || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("actual_type", value ?? "")
|
|
}
|
|
/>
|
|
<div />
|
|
|
|
<Select
|
|
label="项目名称"
|
|
data={projectOptions}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("project_id")}
|
|
value={form.values.project_id || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("project_id", value ?? "")
|
|
}
|
|
/>
|
|
<Select
|
|
label="任务组长"
|
|
data={leaderOptions}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("leader_uid")}
|
|
value={form.values.leader_uid || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("leader_uid", value ?? "")
|
|
}
|
|
/>
|
|
<Select
|
|
label="工作类别"
|
|
data={workTypeOpts}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("work_type")}
|
|
value={form.values.work_type || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("work_type", value ?? "")
|
|
}
|
|
/>
|
|
|
|
<Select
|
|
label="对象类型"
|
|
data={objTypeOpts}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("obj_type")}
|
|
value={form.values.obj_type || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("obj_type", value ?? "")
|
|
}
|
|
/>
|
|
<Select
|
|
label="绩效核算"
|
|
data={performanceOpts}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("performance_accounting")}
|
|
value={form.values.performance_accounting || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("performance_accounting", value ?? "")
|
|
}
|
|
/>
|
|
<Select
|
|
label="核算方式"
|
|
data={accountTypeOpts}
|
|
searchable
|
|
clearable
|
|
{...form.getInputProps("accounting_type")}
|
|
value={form.values.accounting_type || null}
|
|
onChange={(value) =>
|
|
form.setFieldValue("accounting_type", value ?? "")
|
|
}
|
|
/>
|
|
|
|
<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={() => {
|
|
resetFormState()
|
|
onCloseAction()
|
|
}}>
|
|
取消
|
|
</Button>
|
|
<Button type="submit">提交</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Paper>
|
|
</form>
|
|
</Modal>
|
|
)
|
|
}
|