safety-eval-service-frontend/src/pages/Container/Supervision/BasicInfo/OrgAccount/index.js

766 lines
26 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 {
Button,
Checkbox,
DatePicker,
Descriptions,
Form,
Input,
InputNumber,
message,
Modal,
Row,
Col,
Select,
Space,
Tag,
Table,
} from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { tools } from "@cqsjjb/jjb-common-lib";
import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { useEffect, useState } from "react";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import SearchForm from "~/components/SearchForm";
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { QUALIFICATION_INDUSTRY_OPTIONS } from "~/enumerate/enterpriseOptions";
import { isOrgAccountEnabled } from "~/utils/enterpriseInfo/adapter";
import {
CHONGQING_DISTRICTS,
ORG_ACCOUNT_STATE_OPTIONS,
QUALIFICATION_INDUSTRY_OPTIONS_MAP
} from "~/enumerate/enterpriseOptions";
import { NS_ORG_INFO } from "~/enumerate/namespace";
import AttachmentUpload from "~/components/AttachmentUpload";
import { safeListRequest, safeRequest } from "~/utils";
import {
creditCodeRule,
nonNegativeIntegerRule,
normalizeUrl,
phoneRule,
positiveNumberRule,
urlRule,
} from "~/utils/validators";
const { router } = tools;
const CONTACT_SEP = "/";
const mergeContactNamePhone = (name, phone) => {
if (!name && !phone) return undefined;
return [name, phone].filter(Boolean).join(CONTACT_SEP);
};
const splitContactNamePhone = (value) => {
if (!value) return { contactName: undefined, contactPhone: undefined };
const idx = String(value).indexOf(CONTACT_SEP);
if (idx >= 0) {
return {
contactName: String(value).slice(0, idx).trim(),
contactPhone: String(value).slice(idx + 1).trim(),
};
}
return { contactName: String(value).trim(), contactPhone: undefined };
};
function OrgAccountPage(props) {
const [searchForm] = Form.useForm();
const [editForm] = Form.useForm();
const [viewOpen, setViewOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [currentId, setCurrentId] = useState(null);
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [editLoading, setEditLoading] = useState(false);
const [dataSource, setDataSource] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const raw = searchForm.getFieldsValue();
const params = {
...raw,
current: router.query.current || 1,
pageSize: router.query.pageSize || 10,
state:
raw.state !== "" && raw.state !== undefined
? Number(raw.state)
: undefined,
...(Array.isArray(raw.openTimeRange) &&
raw.openTimeRange[0] &&
raw.openTimeRange[1]
? {
createTimeStart: raw.openTimeRange[0].format("YYYY-MM-DD"),
createTimeEnd: raw.openTimeRange[1].format("YYYY-MM-DD"),
}
: {}),
};
delete params.openTimeRange;
Object.keys(params).forEach((k) => {
if (params[k] === undefined) delete params[k];
});
const res = await safeListRequest(props.orgAccountList)(params);
setDataSource(res?.data || []);
setTotal(res?.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => {
getData(1, 10);
}, []);
const loadDetail = async (id) => {
setDetailLoading(true);
const res = await safeRequest(props.orgAccountGet, { id });
setDetail(res?.data || null);
setDetailLoading(false);
return res;
};
const openView = async (id) => {
setCurrentId(id);
setViewOpen(true);
await loadDetail(id);
};
const openEdit = async (id) => {
setCurrentId(id);
setEditOpen(true);
const res = await loadDetail(id);
if (res?.data) {
let attachmentUrls = res.data.attachmentUrls;
if (typeof attachmentUrls === "string" && attachmentUrls) {
try { attachmentUrls = JSON.parse(attachmentUrls); } catch {}
}
editForm.setFieldsValue({
...res.data,
attachmentUrls,
contactNamePhone: mergeContactNamePhone(
res.data.contactName,
res.data.contactPhone,
),
});
}
};
const onToggleStatus = (record) => {
const enabled = isOrgAccountEnabled(record);
Modal.confirm({
title: "确认操作",
content: enabled
? `确认禁用「${record.unitName}」账号?`
: `确认启用「${record.unitName}」账号?`,
okText: "确认",
cancelText: "取消",
onOk: async () => {
const nextState = enabled ? 1 : 0;
const res = await props.orgAccountUpdateState({
id: record.id,
state: nextState,
});
if (res?.success !== false) {
message.success("操作成功");
await getData();
}
},
});
};
const onResetPassword = (record) => {
console.log(record);
Modal.confirm({
title: "重置密码",
content: (
<div>
<p>
确认将
<strong>{record.unitName}</strong>
的密码重置为初始密码
</p>
<p style={{ color: "rgba(0,0,0,0.45)", fontSize: 14 }}>
初始密码a123456
</p>
</div>
),
okText: "确认重置",
cancelText: "取消",
onOk: async () => {
const res = await props.orgAccountResetPassword({ data: record.id });
if (res?.success !== false) {
message.success("密码已重置");
}
},
});
};
const onDelete = (record) => {
Modal.confirm({
title: "确认删除",
content: (
<div>
<p>
确认删除
<strong>{record.unitName}</strong>
的账号
</p>
<p style={{ color: "rgba(0,0,0,0.45)", fontSize: 14 }}>
此操作不可恢复请谨慎操作
</p>
</div>
),
okText: "确认删除",
okButtonProps: { danger: true },
cancelText: "取消",
onOk: async () => {
const res = await props.orgAccountDelete({ data: record.id });
if (res?.success !== false) {
message.success("删除成功");
await getData();
}
},
});
};
const onEditSubmit = async () => {
const formValues = await editForm.validateFields();
const split = splitContactNamePhone(formValues.contactNamePhone);
const contactName = split.contactName
? split.contactName.replace(/\s+/g, "")
: undefined;
const contactPhone = split.contactPhone
? split.contactPhone.replace(/\s+/g, "")
: undefined;
const values = {
...formValues,
contactName,
contactPhone,
contactNamePhone: undefined,
attachmentUrls: formValues.attachmentUrls
? JSON.stringify(formValues.attachmentUrls)
: undefined,
infoDisclosureUrl: formValues.infoDisclosureUrl
? normalizeUrl(formValues.infoDisclosureUrl)
: undefined,
id: currentId,
};
setEditLoading(true);
const res = await props.orgAccountModify(values);
setEditLoading(false);
if (res?.success !== false) {
message.success("保存成功");
setEditOpen(false);
setCurrentId(null);
editForm.resetFields();
await getData();
}
};
const columns = [
{ title: "单位名称", dataIndex: "unitName", ellipsis: true },
{
title: "拟申请的法定安全评价业务范围",
dataIndex: "safetyIndustryCategoryCode",
ellipsis: true,
render: (codes) =>
(Array.isArray(codes) ? codes : [])
.map((code) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[code] || code)
.join("、") || "-",
},
{ title: "办公地址", dataIndex: "businessAddress", ellipsis: true },
{
title: "状态",
width: 80,
render: (_, record) => (
<Tag color={isOrgAccountEnabled(record) ? "success" : "error"}>
{isOrgAccountEnabled(record) ? "启用" : "禁用"}
</Tag>
),
},
{ title: "开户时间", dataIndex: "createTime", width: 155 },
{
title: "操作",
width: 200,
fixed: "right",
render: (_, record) => (
<TableAction>
<Button type="link" size="small" onClick={() => openView(record.id)}>
查看
</Button>
<Button type="link" size="small" onClick={() => openEdit(record.id)}>
编辑
</Button>
<Button
type="link"
size="small"
onClick={() => onResetPassword(record)}
>
重置密码
</Button>
<Button
type="link"
size="small"
danger
onClick={() => onDelete(record)}
>
删除
</Button>
<Button
type="link"
size="small"
onClick={() => onToggleStatus(record)}
>
{isOrgAccountEnabled(record) ? "禁用" : "启用"}
</Button>
</TableAction>
),
},
];
return (
<PageLayout
title={
<div>
<span>机构账号管理</span>
<div className="pageLayout-extra">监管端维护评价机构开户账号</div>
</div>
}
>
<SearchForm
style={{ marginBottom: 24 }}
form={searchForm}
loading={loading}
formLine={[
<Form.Item key="unitName" name="unitName">
<ControlWrapper.Input
label="机构名称"
placeholder="关键字搜索"
allowClear
/>
</Form.Item>,
<Form.Item key="districtName" name="districtName">
<ControlWrapper.Select
label="属地"
placeholder="请选择属地"
allowClear
>
{CHONGQING_DISTRICTS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
<Form.Item key="state" name="state">
<ControlWrapper.Select
label="状态"
placeholder="请选择状态"
allowClear
>
{ORG_ACCOUNT_STATE_OPTIONS.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
<Form.Item key="openTimeRange" name="openTimeRange">
<ControlWrapper.DatePicker.RangePicker
label="开户时间"
allowClear
/>
</Form.Item>,
]}
onReset={(values) => {
searchForm.resetFields();
router.query = {
...values,
current: 1,
pageSize: 10,
};
getData();
}}
onFinish={(values) => {
router.query = {
...values,
current: 1,
pageSize: 10,
};
getData();
}}
/>
<Table
rowKey="id"
columns={columns}
dataSource={dataSource}
scroll={{ y: props.scrollY }}
loading={loading}
pagination={{
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: router.query.current || 1,
pageSize: router.query.pageSize || 10,
onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, pageSize };
getData();
},
}}
/>
<Modal
open={viewOpen}
destroyOnHidden
title="机构信息查看"
width={1000}
loading={detailLoading}
cancelText="关闭"
okButtonProps={{ style: { display: "none" } }}
onCancel={() => {
setViewOpen(false);
setDetail(null);
}}
>
{detail && (
<Descriptions bordered column={2} size="small" labelStyle={{ width: 165 }}>
<Descriptions.Item label="单位名称">
{detail.unitName}
</Descriptions.Item>
<Descriptions.Item label="统一社会信用代码">
{detail.creditCode}
</Descriptions.Item>
<Descriptions.Item label="营业执照" span={2}>
{(() => {
let files = [];
if (Array.isArray(detail.attachmentUrls)) {
files = detail.attachmentUrls;
} else if (typeof detail.attachmentUrls === "string" && detail.attachmentUrls) {
try { files = JSON.parse(detail.attachmentUrls); } catch {}
}
if (!files.length) return "-";
return files.map((f, i) => (
<a key={i} href={f.url} target="_blank" rel="noreferrer" style={{ marginRight: 8 }}>
{f.name || f.url}
</a>
));
})()}
</Descriptions.Item>
<Descriptions.Item label="注册地址" span={2}>
{detail.registerAddress || "-"}
</Descriptions.Item>
<Descriptions.Item label="办公地址" span={2}>
{detail.businessAddress || "-"}
</Descriptions.Item>
<Descriptions.Item label="信息公开网址">
{detail.infoDisclosureUrl || "-"}
</Descriptions.Item>
<Descriptions.Item label="资质证书编号">
{detail.qualificationCertNo || "-"}
</Descriptions.Item>
<Descriptions.Item label="法定代表人">
{detail.legalRepresentative || "-"}
</Descriptions.Item>
<Descriptions.Item label="法定代表人电话">
{detail.legalRepresentativePhone || "-"}
</Descriptions.Item>
<Descriptions.Item label="传真">
{detail.fax || "-"}
</Descriptions.Item>
<Descriptions.Item label="联系人及电话">
{mergeContactNamePhone(detail.contactName, detail.contactPhone) || "-"}
</Descriptions.Item>
<Descriptions.Item label="固定资产总值(万元)">
{detail.fixedAssetsTotal ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="工作场所建筑面积(㎡)">
{detail.workplaceArea ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="档案室面积(㎡)">
{detail.archiveRoomArea ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="专职安全评价师数量">
{detail.fulltimeEvaluatorCount ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="注册安全工程师数量">
{detail.registeredEngineerCount ?? "-"}
</Descriptions.Item>
<Descriptions.Item label="拟申请的法定安全评价业务范围" span={2}>
{(detail.safetyIndustryCategoryCode || [])
.map((code) => QUALIFICATION_INDUSTRY_OPTIONS_MAP[code] || code)
.join("、") || "-"}
</Descriptions.Item>
<Descriptions.Item label="单位基本情况介绍" span={2}>
{detail.orgIntro || "-"}
</Descriptions.Item>
</Descriptions>
)}
</Modal>
<Modal
open={editOpen}
destroyOnHidden
title="编辑机构信息"
width={900}
confirmLoading={editLoading}
okText="保存"
cancelText="取消"
onCancel={() => {
setEditOpen(false);
setCurrentId(null);
editForm.resetFields();
}}
onOk={onEditSubmit}
>
<Form form={editForm} layout="vertical" scrollToFirstError>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="unitName"
label="单位名称"
rules={[{ required: true, message: "请输入单位名称" }]}
>
<Input placeholder="请输入单位名称" allowClear maxLength={200} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="creditCode"
label="统一社会信用代码"
rules={[creditCodeRule(true)]}
>
<Input placeholder="请输入统一社会信用代码" allowClear maxLength={18} />
</Form.Item>
</Col>
<Col span={24}>
<AttachmentUpload
name="attachmentUrls"
label="营业执照"
accept=".pdf,.jpg,.jpeg,.png"
maxCount={5}
extra="支持上传营业执照扫描件限定pdf和图片最多5个。"
rules={[{ required: true, message: "请上传营业执照" }]}
/>
</Col>
<Col span={24}>
<Form.Item
name="registerAddress"
label="注册地址"
rules={[{ required: true, message: "请输入注册地址" }]}
>
<Input placeholder="请输入注册地址" maxLength={500} />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="businessAddress"
label="办公地址"
rules={[{ required: true, message: "请输入办公地址" }]}
>
<Input.TextArea rows={3} placeholder="请输入办公地址" maxLength={500} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="infoDisclosureUrl"
label="信息公开网址"
rules={[urlRule("信息公开网址", true)]}
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入信息公开网址" allowClear maxLength={500} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="qualificationCertNo"
label="资质证书编号"
extra="非必填,初次申请无需填写"
>
<Input placeholder="初次申请无需填写" allowClear maxLength={100} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="legalRepresentative"
label="法定代表人"
rules={[{ required: true, message: "请输入法定代表人" }]}
>
<Input placeholder="请输入法定代表人" allowClear maxLength={50} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="legalRepresentativePhone"
label="法定代表人电话"
rules={[phoneRule("法定代表人电话", true)]}
normalize={(value) =>
typeof value === "string" ? value.trim() : value
}
>
<Input placeholder="请输入法定代表人电话" allowClear maxLength={20} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="fax" label="传真" rules={[{ required: true, message: "请输入传真" }]}>
<Input placeholder="请输入传真" allowClear maxLength={20} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="contactNamePhone"
label="联系人及电话"
validateFirst
rules={[
{ required: true, message: "请输入联系人及电话" },
{
validator: (_, value) => {
const { contactName, contactPhone } =
splitContactNamePhone(value);
if (!contactName) {
return Promise.reject(new Error("联系人不能为空"));
}
if (contactName.length > 50) {
return Promise.reject(new Error("联系人不能超过50字"));
}
if (!contactPhone) {
return Promise.reject(
new Error("请使用/分隔联系人和电话,并填写电话"),
);
}
if (
!/^(1[3-9]\d{9}|\d{10,12})$/.test(
String(contactPhone).replace(/\s+/g, ""),
)
) {
return Promise.reject(
new Error("电话格式不正确手机11位或固话区号+号码)"),
);
}
if (contactPhone.length > 20) {
return Promise.reject(new Error("电话不能超过20字"));
}
return Promise.resolve();
},
},
]}
extra="请使用/分隔联系人和电话,如:张三/13800138000"
>
<Input
placeholder="如:张三/13800138000"
allowClear
maxLength={73}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="fixedAssetsTotal"
label="固定资产总值(万元)"
rules={[
{ required: true, message: "请输入固定资产总值" },
{
validator: (_, value) => {
if (value === undefined || value === null || value === "") {
return Promise.resolve();
}
if (Number(value) < 0) {
return Promise.reject(new Error("固定资产总值应为非负数字"));
}
return Promise.resolve();
},
},
]}
>
<InputNumber style={{ width: "100%" }} min={0} precision={4} max={99999999999} allowClear placeholder="请输入固定资产总值" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="workplaceArea"
label="工作场所建筑面积(㎡)"
rules={[positiveNumberRule("工作场所建筑面积", true)]}
>
<InputNumber style={{ width: "100%" }} min={0} max={99999} precision={2} placeholder="请输入工作场所建筑面积" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="archiveRoomArea"
label="档案室面积(㎡)"
rules={[positiveNumberRule("档案室面积", true)]}
>
<InputNumber style={{ width: "100%" }} min={0} max={99999} precision={2} placeholder="请输入档案室面积" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="fulltimeEvaluatorCount"
label="专职安全评价师数量"
rules={[nonNegativeIntegerRule("专职安全评价师数量", true)]}
>
<InputNumber style={{ width: "100%" }} min={0} max={99999} precision={0} placeholder="请输入专职安全评价师数量" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="registeredEngineerCount"
label="注册安全工程师数量"
rules={[nonNegativeIntegerRule("注册安全工程师数量", true)]}
>
<InputNumber style={{ width: "100%" }} min={0} max={99999} precision={0} placeholder="请输入注册安全工程师数量" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="safetyIndustryCategoryCode"
label="拟申请的法定安全评价业务范围"
rules={[{ required: true, message: "请选择拟申请的法定安全评价业务范围" }]}
>
<Checkbox.Group
style={{
display: "grid",
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
gap: "9.1px 14px",
padding: "14px",
border: "1px solid #e2e8f0",
borderRadius: 8,
background: "#f8fafc",
}}
>
{QUALIFICATION_INDUSTRY_OPTIONS.map((opt) => (
<Checkbox key={opt.value} value={opt.value}>
{opt.label}
</Checkbox>
))}
</Checkbox.Group>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="orgIntro"
label="单位基本情况介绍(可附页)"
rules={[{ required: true, message: "请输入单位基本情况介绍" }]}
>
<Input.TextArea
rows={7}
placeholder="请简要介绍单位基本情况,如成立时间、人员构成、技术力量、业绩等"
maxLength={2000}
showCount
/>
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</PageLayout>
);
}
export default Connect(
[NS_ORG_INFO],
true,
)(AntdTableFuncControl(OrgAccountPage));