safety-eval-service-frontend/src/pages/Container/QualificationReview/FilingTabs/index.js

555 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useMemo } from "react";
import { Button, Image, Input, Select, Table, Tabs, Tag } from "antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_REVIEW, NS_EQUIP_INFO } from "~/enumerate/namespace";
import StaffViewModal from "~/components/StaffViewModal";
import PreviewUrlButton from "~/components/PreviewUrlButton/index";
import { EquipViewModal } from "~/pages/Container/EnterpriseInfo/EquipInfo";
import {
QUALIFICATION_INDUSTRY_OPTIONS,
QUALIFICATION_INDUSTRY_OPTIONS_MAP,
TITLE_LEVEL_MAP,
} from "~/enumerate/enterpriseOptions";
import { CAPABILITY_MAP } from "~/enumerate/constant";
import { getCalibrationTag } from "../mockData";
const { TextArea } = Input;
/**
* 共享组件:资质备案初审 6 个标签页(详情 / 审核共用)
* 对应原型 mod-qual-review-form / mod-qual-review-detail
* 1. 申请基本信息 2. 管理人员 3. 专职安全评价师
* 4. 申请材料 5. 设备清单 6. 机构负责人签字
* @param {Object} props
* @param {Object} props.detail - 备案详情数据
* @param {boolean} [props.isReview=false] - 审核模式(材料表格开启"符合性"列)
* @param {Object} [props.compliance={}] - 审核模式下的符合性状态
* @param {Function} [props.onComplianceChange] - 符合性变动回调
*/
function calcAge(birthDate) {
if (!birthDate) return "-";
const birth = new Date(birthDate);
if (isNaN(birth.getTime())) return "-";
const now = new Date();
let age = now.getFullYear() - birth.getFullYear();
const m = now.getMonth() - birth.getMonth();
if (m < 0 || (m === 0 && now.getDate() < birth.getDate())) age--;
return age >= 0 ? age : "-";
}
/**
* 职称名称:后端 QualFilingPersonnelInfoCO 返回 titleCodeArr编码数组
* 转中文名称展示(高级/中级/初级。变更时间2026-08-10对齐后端字段
*/
function titleNames(titleCodeArr) {
// 兼容:初审返回数组,备案变更返回字符串(可能为 "SENIOR" / "SENIOR,MIDDLE" / JSON 数组字符串。变更时间2026-08-10
let list = Array.isArray(titleCodeArr) ? titleCodeArr : [];
if (!Array.isArray(titleCodeArr) && typeof titleCodeArr === "string") {
try {
const parsed = JSON.parse(titleCodeArr);
list = Array.isArray(parsed) ? parsed : [titleCodeArr];
} catch {
list = String(titleCodeArr)
.split(/[,,、]/)
.map((s) => s.trim())
.filter(Boolean);
}
}
const names = list
.map((item) => {
const code = typeof item === "string" ? item : item?.titleCode || item?.title;
return item?.titleName || TITLE_LEVEL_MAP[code] || code || "";
})
.filter(Boolean);
// 空数组返回空字符串,便于调用方回退 titleName 等字段
return names.length ? names.join("、") : "";
}
/**
* 从业年限:后端返回 joinWorkDate参加工作时间按当前日期推算年数。
* 变更时间2026-08-10对齐后端字段
*/
function workYearsText(joinWorkDate) {
if (!joinWorkDate) return "-";
const d = new Date(String(joinWorkDate).replace(/-/g, "/"));
if (Number.isNaN(d.getTime())) return "-";
const now = new Date();
let years = now.getFullYear() - d.getFullYear();
const m = now.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) years--;
return years >= 0 ? `${years}` : "-";
}
const gridStyle = {
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "0.6rem 1rem",
};
const fieldStyle = { display: "flex", flexDirection: "column", gap: "0.3rem" };
const labelStyle = { fontSize: "0.8rem", color: "#64748b", fontWeight: 500 };
const inputStyle = { background: "#f8fafc" };
const fullSpan = { gridColumn: "1 / -1" };
/** 只读字段 */
function ReadonlyField({ label, value, span }) {
return (
<div style={span ? { ...fieldStyle, ...span } : fieldStyle}>
<label style={labelStyle}>{label}</label>
<Input readOnly value={value || "-"} style={inputStyle} />
</div>
);
}
const FilingTabs = ({
detail = {},
isReview = false,
compliance = {},
onComplianceChange,
/**
* 额外 Tab如机构备案详情「7. 变更记录」),数组元素与 antd Tabs items 同构。
* 不传时保持原 6 Tab不影响初审/确认/专家核验展示。变更时间2026-08-10
*/
extraTabs,
equipInfoGet,
}) => {
const [viewId, setViewId] = useState("");
const [equipViewOpen, setEquipViewOpen] = useState(false);
const [equipId, setEquipId] = useState("");
const personnelRows = useMemo(
() =>
(detail.personnelList || []).map((item) => ({
...item,
age: item.age ?? calcAge(item.birthDate),
})),
[detail.personnelList],
);
/**
* 管理人员判定technicalDirectorFlag(技术负责人) 或 processControlLeaderFlag(过程控制负责人) 为 true
* 法定代表人/负责人按岗位名兜底归入管理人员。
* 备案变更详情人员 COQualFilingPersonnelChangeInfoCO暂未返回 flag 字段,靠岗位名兜底判断(待后端补充 flag
* 变更时间2026-08-10按后端人员标识字段拆分
*/
const isManager = (item) =>
item.technicalDirectorFlag === true ||
item.processControlLeaderFlag === true ||
/法定代表人|负责人/.test(item.positionName || "");
const managerRows = useMemo(() => {
return personnelRows.filter(isManager);
}, [personnelRows]);
/** 专职安全评价师:非管理人员的其余人员 */
const evaluatorRows = useMemo(() => {
return personnelRows.filter((item) => !isManager(item));
}, [personnelRows]);
const attachmentList = useMemo(() => {
const v = detail.attachmentUrl;
if (!v) return [];
try {
const p = JSON.parse(v);
return Array.isArray(p) ? p : [];
} catch {
return [];
}
}, [detail.attachmentUrl]);
const businessScope = useMemo(() => {
const list = Array.isArray(detail.businessScope) ? detail.businessScope : [];
return QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => ({
...opt,
checked: list.includes(opt.value),
}));
}, [detail.businessScope]);
/** 人员表格列(管理/评价师共用基础) */
const basePersonCols = (withScope) => [
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
{ title: "姓名", dataIndex: "personName", width: 110 },
{ title: "岗位类别", dataIndex: "positionName", width: 140, ellipsis: true },
{
title: "学历/专业",
width: 160,
// 后端字段educationName(学历) / basicDisciplineMajorName(专业)变更时间2026-08-10
render: (_, r) =>
[r.educationName, r.basicDisciplineMajorName].filter(Boolean).join(" / ") || "-",
},
{
title: "职称",
width: 120,
render: (_, r) => titleNames(r.titleCodeArr) || r.titleName || "-",
},
{
title: "从业年限",
width: 100,
render: (_, r) =>
r.workYears || r.yearsOfWorking || workYearsText(r.joinWorkDate),
},
{
title: "专业能力",
width: 180,
render: (_, r) => {
// 能力编码:兼容 professionalCapabilityCodeList 与后端 capabilityAssessment变更时间2026-08-10
// 备案变更返回 capabilityAssessment 字符串JSON 数组),需解析后映射
let capList = r.capabilityAssessment;
if (typeof capList === "string") {
try {
const parsed = JSON.parse(capList);
capList = Array.isArray(parsed) ? parsed : [];
} catch {
capList = [];
}
}
const codes =
r.professionalCapabilityCodeList ||
(Array.isArray(capList)
? capList.map((c) => (typeof c === "string" ? c : c?.professionalCapabilityCode)).filter(Boolean)
: []) ||
[];
return codes.length
? codes.map((code) => CAPABILITY_MAP[code] || code).join("、")
: "-";
},
},
...(withScope
? [
{
title: "申请业务范围",
width: 200,
ellipsis: true,
render: (_, r) => {
const scopes = r.appliedScope || r.scopeList;
if (Array.isArray(scopes)) {
return scopes.map((s) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[s] || s).join("、");
}
return scopes || "-";
},
},
]
: []),
{
title: "操作",
width: 80,
fixed: "right",
render: (_, record) => (
<Button
type="link"
size="small"
onClick={() => setViewId(record.sourcePersonnelId || record.id)}
>
查看
</Button>
),
},
];
const materialCols = isReview
? [
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
{ title: "申请材料", dataIndex: "materialContent", ellipsis: true },
{ title: "格式", dataIndex: "materialFormat", width: 80 },
{
title: "状态",
width: 100,
render: (_, record) =>
record.uploadStatusCode === 2 ? (
<Tag color="success">{record.uploadStatusName || "已上传"}</Tag>
) : (
<Tag color="warning">{record.uploadStatusName || "待上传"}</Tag>
),
},
{
title: "符合性",
width: 110,
render: (_, record) => (
<Select
style={{ width: "100%", fontSize: "0.75rem" }}
size="small"
placeholder="请选择"
value={compliance[record.id]}
allowClear
defaultValue="pass"
onChange={(v) => onComplianceChange?.(record.id, v)}
>
<Select.Option value="pass">符合</Select.Option>
<Select.Option value="fail">不符合</Select.Option>
</Select>
),
},
{
title: "操作",
width: 80,
render: (_, record) => (
<PreviewUrlButton url={record.attachmentUrl}>预览</PreviewUrlButton>
),
},
]
: [
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
{ title: "申请材料", dataIndex: "materialContent", ellipsis: true },
{ title: "格式", dataIndex: "materialFormat", width: 80 },
{
title: "操作",
width: 80,
render: (_, record) => (
<PreviewUrlButton url={record.attachmentUrl}>预览</PreviewUrlButton>
),
},
];
const commitment = detail.commitment || {};
const items = [
{
key: "info",
label: "1. 申请基本信息",
children: (
<div style={gridStyle}>
<div style={{ ...fieldStyle, ...fullSpan }}>
<label style={labelStyle}>拟申请的法定安全评价业务范围 *</label>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "0.3rem 1rem",
padding: "0.45rem 0.75rem",
border: "1px solid #d9d9d9",
borderRadius: 6,
background: "#f8fafc",
}}
>
{businessScope.map((opt) => (
<label
key={opt.value}
style={{ fontSize: "0.82rem", color: "#1e293b", cursor: "not-allowed" }}
>
<input type="checkbox" checked={opt.checked} disabled /> {opt.label}
</label>
))}
</div>
</div>
<ReadonlyField label="备案属地 *" value={detail.filingTerritoryName} />
<ReadonlyField label="备案单位 *" value={detail.filingUnitName} />
<ReadonlyField label="备案单位类型 *" value={detail.filingUnitTypeName} />
<ReadonlyField label="统一社会信用代码 *" value={detail.creditCode} />
<ReadonlyField label="注册地址 *" value={detail.registerAddress} span={fullSpan} />
<ReadonlyField label="办公地址 *" value={detail.officeAddress} span={fullSpan} />
<ReadonlyField label="信息公开网址 *" value={detail.infoDisclosureUrl} />
<div style={fieldStyle}>
<label style={labelStyle}>资质证书编号 *</label>
<Input readOnly value={detail.qualCertNo || "-"} style={inputStyle} />
<span style={{ color: "#8b95a5", fontSize: "0.72rem" }}>
初次申请无需填写已有资质备案时填写
</span>
</div>
{/* 法定代表人:后端 CO 字段为 legalRepresentative表 legal_representative变更时间2026-08-10 */}
<ReadonlyField
label="法定代表人 *"
value={detail.legalRepresentative || detail.legalPersonName || detail.legalPerson}
/>
<ReadonlyField label="法定代表人电话 *" value={detail.legalPersonPhone} />
<ReadonlyField label="传真 *" value={detail.fax} />
<ReadonlyField label="联系人及电话 *" value={detail.contactPhone} />
<ReadonlyField label="固定资产总值(万元) *" value={detail.fixedAssetAmount} />
<ReadonlyField label="工作场所建筑面积(㎡) *" value={detail.workplaceArea} />
<ReadonlyField label="档案室面积(㎡) *" value={detail.archiveRoomArea} />
<ReadonlyField label="专职安全评价师数量 *" value={detail.fulltimeEvaluatorCount} />
<ReadonlyField label="注册安全工程师数量 *" value={detail.registeredEngineerCount} />
<div style={{ ...fieldStyle, ...fullSpan }}>
<label style={labelStyle}>单位基本情况介绍可附页 *</label>
<TextArea readOnly rows={5} value={detail.unitIntro || ""} style={inputStyle} />
</div>
<div style={{ ...fieldStyle, ...fullSpan }}>
<label style={labelStyle}>营业执照</label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
{attachmentList.length ? (
<>
<PreviewUrlButton url={attachmentList[0].url} size="small">
查看营业执照
</PreviewUrlButton>
<span style={{ color: "#8b95a5", fontSize: "0.75rem" }}>
{attachmentList[0].name || "营业执照.pdf"}
</span>
</>
) : (
<span style={{ color: "#8b95a5", fontSize: "0.8rem" }}></span>
)}
</div>
</div>
</div>
),
},
{
key: "management",
label: "2. 管理人员基本情况汇总表",
children: (
<Table
size="small"
bordered
rowKey="id"
pagination={false}
dataSource={managerRows}
scroll={{ x: 900 }}
columns={basePersonCols(false)}
/>
),
},
{
key: "evaluators",
label: "3. 专职安全评价师基本情况汇总表",
children: (
<Table
size="small"
bordered
rowKey="id"
pagination={false}
dataSource={evaluatorRows}
scroll={{ x: 1100 }}
columns={basePersonCols(true)}
/>
),
},
{
key: "materials",
label: "4. 申请材料",
children: (
<Table
size="small"
bordered
rowKey="id"
pagination={false}
dataSource={Array.isArray(detail.materials) ? detail.materials : []}
columns={materialCols}
/>
),
},
{
key: "equipment",
label: "5. 设备清单",
children: (
<Table
size="small"
bordered
rowKey="id"
pagination={false}
dataSource={Array.isArray(detail.equipmentList) ? detail.equipmentList : []}
columns={[
{ title: "序号", width: 60, render: (_, __, i) => i + 1 },
{ title: "装备名称", dataIndex: "deviceName", ellipsis: true },
{ title: "规格型号", dataIndex: "deviceModel", ellipsis: true },
{ title: "生产厂家", dataIndex: "manufacturer" },
{
title: "计量检定情况",
width: 120,
render: (_, record) => {
const tag = getCalibrationTag(record);
return (
<Tag color={tag.status === "success" ? "success" : "warning"}>
{tag.label}
</Tag>
);
},
},
{
title: "操作",
width: 80,
render: (_, record) => (
<Button
type="link"
size="small"
onClick={() => {
setEquipId(record.sourceEquipmentId);
setEquipViewOpen(true);
}}
>
查看
</Button>
),
},
]}
/>
),
},
{
key: "signature",
label: "6. 机构负责人签字",
children: (
<div
style={{
border: "1px solid #d9d9d9",
borderRadius: 8,
padding: "1.25rem 1.5rem",
background: "#fafafa",
fontSize: "0.85rem",
lineHeight: 1.9,
}}
>
<p style={{ textAlign: "center", fontWeight: 600, fontSize: "0.95rem", marginBottom: "0.75rem" }}>
机构负责人签字确认
</p>
<p>
机构端已完成资质申请第六项负责人签字确认本次提交的基础信息管理人员专职安全评价师申请材料和设备清单真实准确完整
</p>
<div style={gridStyle}>
<ReadonlyField label="签字人" value={commitment.legalRepName} />
<ReadonlyField label="职务" value="法定代表人 / 机构负责人" />
<ReadonlyField label="签字方式" value="APP认证签字" />
<ReadonlyField label="签字时间" value={commitment.signDate} />
<div style={{ ...fieldStyle, ...fullSpan }}>
<label style={labelStyle}>签字文件</label>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
{commitment.legalRepSignatureUrl ? (
<>
<PreviewUrlButton url={commitment.legalRepSignatureUrl} size="small">
查看签字件
</PreviewUrlButton>
<Tag color="success">已签字</Tag>
</>
) : (
<Tag color="warning">未签字</Tag>
)}
</div>
</div>
{commitment.legalRepSignatureUrl && (
<Image
src={commitment.legalRepSignatureUrl}
style={{ width: 120, height: 120, objectFit: "contain", marginTop: 8 }}
/>
)}
</div>
</div>
),
},
...(Array.isArray(extraTabs) ? extraTabs : []),
];
return (
<>
<Tabs items={items} />
<StaffViewModal
open={!!viewId}
currentId={viewId}
onCancel={() => setViewId("")}
/>
{equipViewOpen && (
<EquipViewModal
open={equipViewOpen}
currentId={equipId}
equipInfoGet={equipInfoGet}
onCancel={() => {
setEquipViewOpen(false);
setEquipId("");
}}
/>
)}
</>
);
};
export default Connect([NS_QUAL_REVIEW, NS_EQUIP_INFO], true)(FilingTabs);