feat
parent
8b2d97eb47
commit
396a534bf5
|
|
@ -163,6 +163,16 @@ export const evalProjectMemberSave = declareRequest(
|
|||
"Post > @/safetyEval/institution/eval-project-member/save",
|
||||
);
|
||||
|
||||
export const evalProjectMemberParticipantSave = declareRequest(
|
||||
"memberParticipantSaveLoading",
|
||||
"Post > @/safetyEval/institution/eval-project-member/participant/save",
|
||||
);
|
||||
|
||||
export const evalProjectMemberParticipantList = declareRequest(
|
||||
"memberParticipantListLoading",
|
||||
"Get > /safetyEval/institution/eval-project-member/participant/list",
|
||||
);
|
||||
|
||||
// ===== 项目预警(安评业务提醒) =====
|
||||
|
||||
export const projectWaringStat = declareRequest(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Button,
|
||||
message,
|
||||
} from "antd";
|
||||
import { PictureOutlined } from "@ant-design/icons";
|
||||
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
|
||||
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
|
|
@ -231,6 +232,25 @@ const TeamPanel = ({ data, loading }) => {
|
|||
const SurveyPanel = ({ personnel, attachments, loading, detailData }) => {
|
||||
if (loading) return <Empty />;
|
||||
|
||||
|
||||
/** 打卡时间后追加人脸照片图标:faceUrl 逗号分隔,第1张为签到、第2张为签退 */
|
||||
const renderTimeWithFace = (time, record, idx) => {
|
||||
const url = record?.faceUrl?.split(",")[idx];
|
||||
if (!url) return time || "-";
|
||||
return (
|
||||
<span>
|
||||
{time}
|
||||
<Popover
|
||||
content={<Image src={url} width={240} />}
|
||||
title="人脸照片"
|
||||
trigger="hover"
|
||||
>
|
||||
<PictureOutlined style={{ marginLeft: 6, color: "#1677ff", cursor: "pointer" }} />
|
||||
</Popover>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const personnelColumns = [
|
||||
{ title: "人员", dataIndex: "personnelName", width: 100 },
|
||||
{
|
||||
|
|
@ -263,8 +283,8 @@ const SurveyPanel = ({ personnel, attachments, loading, detailData }) => {
|
|||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: "签到时间", dataIndex: "singInTime", width: 140 },
|
||||
{ title: "签退时间", dataIndex: "signOutTime", width: 140 },
|
||||
{ title: "签到时间", dataIndex: "singInTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 0);}},
|
||||
{ title: "签退时间", dataIndex: "signOutTime" , render: (time, record)=>{return renderTimeWithFace(time, record, 1);}},
|
||||
{
|
||||
title: "现场照片",
|
||||
dataIndex: "sceneUrl",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import {
|
|||
Checkbox,
|
||||
message,
|
||||
Spin,
|
||||
Card,
|
||||
Popover,
|
||||
Image,
|
||||
} from "antd";
|
||||
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
||||
import { NS_SAFETY_EVAL_BUSINESS } from "~/enumerate/namespace";
|
||||
|
|
@ -20,10 +23,22 @@ import LayoutFooterPortal from "~/components/LayoutFooterPortal";
|
|||
const { router } = tools;
|
||||
|
||||
const TeamContent = ({ readOnly, ...props }) => {
|
||||
const { safetyEvalBusiness, evalProjectMemberPage, evalProjectMemberSave } =
|
||||
props;
|
||||
const { evalProjectDetailData, memberLoading, memberSaveLoading } =
|
||||
safetyEvalBusiness || {};
|
||||
const {
|
||||
safetyEvalBusiness,
|
||||
evalProjectMemberPage,
|
||||
evalProjectMemberSave,
|
||||
evalProjectMemberParticipantSave,
|
||||
evalProjectMemberParticipantList,
|
||||
evalSignTaskSave,
|
||||
} = props;
|
||||
const {
|
||||
evalProjectDetailData,
|
||||
memberLoading,
|
||||
memberSaveLoading,
|
||||
memberParticipantSaveLoading,
|
||||
memberParticipantListLoading,
|
||||
evalSignTaskSaveLoading,
|
||||
} = safetyEvalBusiness || {};
|
||||
const participants = evalProjectDetailData?.participants || [];
|
||||
const projectId = router.query?.id;
|
||||
|
||||
|
|
@ -31,6 +46,11 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
const [selectedRole, setSelectedRole] = useState(null);
|
||||
const [selectedPersons, setSelectedPersons] = useState([]);
|
||||
const [teamMembers, setTeamMembers] = useState([]);
|
||||
// 参与风险分析人员签字
|
||||
const [participantModalOpen, setParticipantModalOpen] = useState(false);
|
||||
const [selectedParticipantIds, setSelectedParticipantIds] = useState([]);
|
||||
|
||||
const [signers, setSigners] = useState([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!projectId) return;
|
||||
|
|
@ -49,6 +69,11 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
})),
|
||||
);
|
||||
}
|
||||
// 参与签字人员(查询接口:参与人列表)
|
||||
const signRes = await evalProjectMemberParticipantList({ projectId });
|
||||
if (signRes?.success !== false) {
|
||||
setSigners(signRes?.data || []);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -98,16 +123,15 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
return;
|
||||
}
|
||||
// 一人只能担任一个单人唯一角色
|
||||
const uniqueRoleValues = ROLE_DEFINITIONS.filter(
|
||||
(r) => r.unique,
|
||||
).map((r) => r.value);
|
||||
const uniqueRoleValues = ROLE_DEFINITIONS.filter((r) => r.unique).map(
|
||||
(r) => r.value,
|
||||
);
|
||||
const conflict = selectedPersons.find((personId) => {
|
||||
const member = teamMembers.find((m) => m.personnelId === personId);
|
||||
return (
|
||||
member &&
|
||||
member.roles.some(
|
||||
(r) =>
|
||||
uniqueRoleValues.includes(r.role) && r.role !== selectedRole,
|
||||
(r) => uniqueRoleValues.includes(r.role) && r.role !== selectedRole,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
@ -156,14 +180,68 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
setTeamMembers((prev) => prev.filter((m) => m.personnelId !== personnelId));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const allRoles = teamMembers.flatMap((m) => m.roles.map((r) => r.role));
|
||||
if (!allRoles.includes("PROJECT_LEAD")) {
|
||||
message.warning("请先配置项目负责人(项目组组长)");
|
||||
// 发起APP签字
|
||||
const handleSignApp = async (index) => {
|
||||
const participant = signers[index];
|
||||
if (!participant?.personnelId) {
|
||||
message.warning("该人员信息不完整,请先保存后再发起签字");
|
||||
return;
|
||||
}
|
||||
if (!allRoles.includes("PROCESS_CONTROL_LEAD")) {
|
||||
message.warning("请先配置过程控制负责人");
|
||||
// 发起签字前先保存签字人员
|
||||
const saveRes = await evalProjectMemberParticipantSave(
|
||||
signers.map((s, idx) => ({
|
||||
id: s.id,
|
||||
projectId,
|
||||
personnelId: s.personnelId,
|
||||
personnelName: s.personnelName,
|
||||
deptName: s.deptName,
|
||||
signStatusCode: s.signStatusCode,
|
||||
signFileUrl: s.signFileUrl,
|
||||
sortOrder: idx + 1,
|
||||
})),
|
||||
);
|
||||
if (saveRes?.success === false) return;
|
||||
|
||||
try {
|
||||
const res = await evalSignTaskSave({
|
||||
projectId,
|
||||
nodeCode: "PROJECT_GROUP",
|
||||
bizRefId: projectId || undefined,
|
||||
signerPersonnelId: participant.personnelId,
|
||||
taskTitle: "项目组成立签字",
|
||||
taskStatusCode: 1,
|
||||
});
|
||||
if (res?.success) {
|
||||
message.success("已发起APP签字");
|
||||
setSigners((prev) =>
|
||||
prev.map((s, i) => (i === index ? { ...s, signStatusCode: 1 } : s)),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSigner = (personnelId) => {
|
||||
setSigners((prev) => prev.filter((s) => s.personnelId !== personnelId));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const allRoles = teamMembers.flatMap((m) => m.roles.map((r) => r.role));
|
||||
const requiredRoles = [
|
||||
{ value: "REPORT_COMPILER", name: "报告编制人" },
|
||||
{ value: "INTERNAL_AUDITOR", name: "内审人" },
|
||||
{ value: "PROJECT_LEAD", name: "项目负责人(项目组组长)" },
|
||||
{ value: "TECHNICAL_LEAD", name: "技术负责人" },
|
||||
{ value: "PROCESS_CONTROL_LEAD", name: "过程控制负责人" },
|
||||
];
|
||||
const missing = requiredRoles.find((r) => !allRoles.includes(r.value));
|
||||
if (missing) {
|
||||
message.warning(`请先配置${missing.name}(最少1人)`);
|
||||
return;
|
||||
}
|
||||
// 校验签字人员是否已完成签字
|
||||
if (signers.length && !signers.every((s) => s.signStatusCode === 2)) {
|
||||
message.warning("签字人员尚未全部完成签字,请先完成签字后再确认项目组");
|
||||
return;
|
||||
}
|
||||
const payload = teamMembers.map((m) => ({
|
||||
|
|
@ -178,6 +256,22 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
}));
|
||||
const res = await evalProjectMemberSave(payload);
|
||||
if (res?.success !== false) {
|
||||
// 保存签字人员(保存项目经理签字人)
|
||||
if (signers.length) {
|
||||
const signRes = await evalProjectMemberParticipantSave(
|
||||
signers.map((s, idx) => ({
|
||||
id: s.id,
|
||||
projectId,
|
||||
personnelId: s.personnelId,
|
||||
personnelName: s.personnelName,
|
||||
deptName: s.deptName,
|
||||
signStatusCode: s.signStatusCode,
|
||||
signFileUrl: s.signFileUrl,
|
||||
sortOrder: idx + 1,
|
||||
})),
|
||||
);
|
||||
if (signRes?.success === false) return;
|
||||
}
|
||||
message.success("保存成功");
|
||||
fetchData();
|
||||
props.getDetail();
|
||||
|
|
@ -196,7 +290,8 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
render: (roles, record) => (
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
|
||||
{roles.map((r) => {
|
||||
const inOnly = r.role === "PROJECT_LEAD" || r.role === "PROCESS_CONTROL_LEAD";
|
||||
const inOnly =
|
||||
r.role === "PROJECT_LEAD" || r.role === "PROCESS_CONTROL_LEAD";
|
||||
return (
|
||||
<Tag
|
||||
key={r.role}
|
||||
|
|
@ -251,8 +346,69 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
},
|
||||
];
|
||||
|
||||
// 参与风险分析人员签字(去掉分析意见、只能选择一个人)
|
||||
const signerColumns = [
|
||||
{ title: "姓名", dataIndex: "personnelName", ellipsis: true },
|
||||
{ title: "部门/岗位", dataIndex: "deptName", ellipsis: true },
|
||||
{
|
||||
title: "签字状态",
|
||||
render: (_, record, index) => {
|
||||
const value = record.signStatusCode;
|
||||
const signFileUrl = record.signFileUrl;
|
||||
let text = "发起APP签字";
|
||||
if (value === 1) text = "待APP签字";
|
||||
if (value === 2) text = "已手写签字";
|
||||
|
||||
const btn = (
|
||||
<Button
|
||||
size="small"
|
||||
loading={evalSignTaskSaveLoading || memberParticipantSaveLoading}
|
||||
disabled={value === 1 || value === 2 || readOnly}
|
||||
onClick={() => handleSignApp(index)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
);
|
||||
|
||||
// 已签字且有签字图片时,悬停展示签字图片(disabled 按钮不触发 hover,需用 span 包裹)
|
||||
if (value === 2 && signFileUrl) {
|
||||
return (
|
||||
<Popover
|
||||
placement="topRight"
|
||||
content={
|
||||
<Image
|
||||
className="pf-sign-img"
|
||||
src={signFileUrl}
|
||||
alt="签字图片"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span>{btn}</span>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
return btn;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
width: 80,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
disabled={readOnly}
|
||||
onClick={() => handleRemoveSigner(record.personnelId)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Spin spinning={memberLoading}>
|
||||
<Spin spinning={memberLoading || memberParticipantListLoading}>
|
||||
<div>
|
||||
<div className="pf-permission">
|
||||
<Tag color="blue" className="pf-tag">
|
||||
|
|
@ -270,18 +426,16 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
title: "已配置人员",
|
||||
value: `${teamMembers.length} 人`,
|
||||
valueColor: "#1e293b",
|
||||
|
||||
|
||||
iconBg: "#eff6ff",
|
||||
|
||||
},
|
||||
{
|
||||
key: "roleCoverage",
|
||||
title: "已覆盖角色",
|
||||
value: `${new Set(allRoles).size} / 11`,
|
||||
valueColor: "#1677ff",
|
||||
|
||||
|
||||
iconBg: "#e6f4ff",
|
||||
|
||||
},
|
||||
{
|
||||
key: "projectLead",
|
||||
|
|
@ -290,7 +444,6 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
valueColor: allRoles.includes("PROJECT_LEAD")
|
||||
? "#52c41a"
|
||||
: "#faad14",
|
||||
|
||||
},
|
||||
{
|
||||
key: "processControlLead",
|
||||
|
|
@ -301,7 +454,6 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
valueColor: allRoles.includes("PROCESS_CONTROL_LEAD")
|
||||
? "#52c41a"
|
||||
: "#faad14",
|
||||
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
@ -322,6 +474,35 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
scroll={{ x: 1600, y: 320 }}
|
||||
/>
|
||||
|
||||
<Card
|
||||
type="inner"
|
||||
size="small"
|
||||
title="下发至部门经理或负责人签字"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Table
|
||||
dataSource={signers}
|
||||
columns={signerColumns}
|
||||
rowKey="personnelId"
|
||||
pagination={false}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
type="dashed"
|
||||
size="small"
|
||||
style={{ marginTop: 8 }}
|
||||
disabled={signers.length >= 1}
|
||||
onClick={() => {
|
||||
setSelectedParticipantIds([]);
|
||||
setParticipantModalOpen(true);
|
||||
}}
|
||||
>
|
||||
+ 添加参与人员
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<details className="pf-role-guide">
|
||||
<summary className="pf-role-guide-title">
|
||||
查看11类项目角色职责说明
|
||||
|
|
@ -348,7 +529,7 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
<Flex justify="flex-end" gap={8}>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={memberSaveLoading}
|
||||
loading={memberSaveLoading || memberParticipantSaveLoading}
|
||||
onClick={handleSave}
|
||||
>
|
||||
确认项目组
|
||||
|
|
@ -444,6 +625,60 @@ const TeamContent = ({ readOnly, ...props }) => {
|
|||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="选择参与人员"
|
||||
width={650}
|
||||
open={participantModalOpen}
|
||||
onCancel={() => setParticipantModalOpen(false)}
|
||||
onOk={() => {
|
||||
if (signers.length >= 1) {
|
||||
message.warning("该签字人员最多只能配置1人");
|
||||
setParticipantModalOpen(false);
|
||||
return;
|
||||
}
|
||||
const selected = participants.filter((p) =>
|
||||
selectedParticipantIds.includes(p.id),
|
||||
);
|
||||
selected.forEach((p) => {
|
||||
setSigners((prev) => {
|
||||
if (prev.some((s) => s.personnelId === p.id)) return prev;
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
personnelId: p.id,
|
||||
personnelName: p.name,
|
||||
deptName: p.deptName,
|
||||
projectId,
|
||||
},
|
||||
];
|
||||
});
|
||||
});
|
||||
setParticipantModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
dataSource={(() => {
|
||||
const existing = signers.map((s) => s.personnelId);
|
||||
return participants.filter((p) => !existing.includes(p.id));
|
||||
})()}
|
||||
rowKey="id"
|
||||
rowSelection={{
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedParticipantIds,
|
||||
onChange: (keys) => setSelectedParticipantIds(keys),
|
||||
}}
|
||||
columns={[
|
||||
{ title: "姓名", dataIndex: "name", width: 100 },
|
||||
{ title: "部门", dataIndex: "deptName", width: 120 },
|
||||
{ title: "岗位", dataIndex: "posName", width: 120 },
|
||||
{ title: "能力", dataIndex: "capacity", width: 200 },
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
locale={{ emptyText: "所有人员均已添加" }}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue