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

549 lines
19 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, Modal, Select, Table, Tabs, Tag } from "antd";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { NS_QUAL_REVIEW } from "~/enumerate/namespace";
import StaffViewModal from "~/components/StaffViewModal";
import PreviewUrlButton from "~/components/PreviewUrlButton/index";
import { QUALIFICATION_INDUSTRY_OPTIONS, QUALIFICATION_INDUSTRY_OPTIONS_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 : "-";
}
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>
);
}
/**
* 设备详情弹窗(原型 modal-equip-view 缺失,按列表字段补全)
*/
function EquipViewModal({ record, onCancel }) {
const open = !!record;
const calibration = record ? getCalibrationTag(record) : null;
const rows = record
? [
{ label: "装备名称", value: record.deviceName },
{ label: "规格型号", value: record.deviceModel },
{ label: "生产厂家", value: record.manufacturer },
{ label: "设备价值(元)", value: record.equipmentValue },
{ label: "计量检定情况", value: null },
]
: [];
return (
<Modal
open={open}
title="装备详情"
width={560}
cancelText="关闭"
okText="关闭"
onOk={onCancel}
onCancel={onCancel}
footer={
<Button type="primary" onClick={onCancel}>
关闭
</Button>
}
>
{record && (
<div style={gridStyle}>
{rows.map((row) =>
row.value != null ? (
<ReadonlyField key={row.label} label={row.label} value={row.value} />
) : (
<div key={row.label} style={fieldStyle}>
<label style={labelStyle}>{row.label}</label>
<div>
{calibration && (
<Tag
color={calibration.status === "success" ? "success" : "warning"}
>
{calibration.label}
</Tag>
)}
</div>
</div>
),
)}
<div style={{ ...fieldStyle, ...fullSpan }}>
<label style={labelStyle}>检定报告</label>
<div>
{record.calibrationReportUrl ? (
<PreviewUrlButton url={record.calibrationReportUrl}>预览检定报告</PreviewUrlButton>
) : (
<span style={{ color: "#64748b", fontSize: "0.8rem" }}>未上传</span>
)}
</div>
</div>
</div>
)}
</Modal>
);
}
const FilingTabs = ({
detail = {},
isReview = false,
compliance = {},
onComplianceChange,
}) => {
const [viewId, setViewId] = useState("");
const [equipRecord, setEquipRecord] = useState(null);
const [previewImage, setPreviewImage] = useState("");
const personnelRows = useMemo(
() =>
(detail.personnelList || []).map((item) => ({
...item,
age: item.age ?? calcAge(item.birthDate),
})),
[detail.personnelList],
);
/** 管理人员:岗位含"法定代表人/负责人";无匹配时回退展示全部人员 */
const managerRows = useMemo(() => {
const filtered = personnelRows.filter((item) =>
/法定代表人|技术负责人|过程控制负责人|负责人/.test(item.positionName || ""),
);
return filtered.length ? filtered : personnelRows;
}, [personnelRows]);
/** 专职安全评价师:评价师证书号存在或岗位含"评价师";无匹配时回退展示全部人员 */
const evaluatorRows = useMemo(() => {
const filtered = personnelRows.filter(
(item) => item.evaluatorCertNo || /评价师/.test(item.positionName || ""),
);
return filtered.length ? filtered : personnelRows;
}, [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 openPreview = (file) => {
const url = file?.url;
if (!url) return;
if (/\.(jpe?g|png|gif|webp|bmp)(\?|#|$)/i.test(`${file.name || ""} ${url}`.toLowerCase())) {
setPreviewImage(url);
} else {
window.open(url, "_blank");
}
};
/** 人员表格列(管理/评价师共用基础) */
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,
render: (_, r) =>
[r.educationLevelName, r.majorName || r.educationTypeName].filter(Boolean).join(" / ") || "-",
},
{ title: "职称", dataIndex: "titleName", width: 120, render: (v) => v || "-" },
{
title: "从业年限",
width: 100,
render: (_, r) => r.workYears || r.yearsOfWorking || "-",
},
{
title: "专业能力",
width: 180,
render: (_, r) => {
const codes = r.professionalCapabilityCodeList || [];
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>
<ReadonlyField label="法定代表人 *" value={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 ? (
<>
<Button
size="small"
onClick={() => openPreview(attachmentList[0])}
>
查看营业执照
</Button>
<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={() => setEquipRecord(record)}>
查看
</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 ? (
<>
<Button
size="small"
onClick={() =>
openPreview({
url: commitment.legalRepSignatureUrl,
name: "机构负责人签字件",
})
}
>
查看签字件
</Button>
<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>
),
},
];
return (
<>
<Tabs items={items} />
<StaffViewModal
open={!!viewId}
currentId={viewId}
onCancel={() => setViewId("")}
/>
<EquipViewModal record={equipRecord} onCancel={() => setEquipRecord(null)} />
<Image
src={previewImage}
preview={{
visible: !!previewImage,
onVisibleChange: (v) => !v && setPreviewImage(""),
}}
style={{ display: "none" }}
/>
</>
);
};
export default Connect([NS_QUAL_REVIEW], true)(FilingTabs);