index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. import { iAuditHistoryState, iAuditor, iLatestAuditorState } from '@/types/safe'
  2. import { iAccountGroupItem, iUserInfo } from '@/types/setting'
  3. import { auditConsts } from '@/utils/common/constStatus'
  4. import { getUserGroup } from '@/utils/common/user'
  5. import { formatDate } from '@/utils/util'
  6. import { Input, Popover } from 'antd'
  7. import QueueAnim from 'rc-queue-anim'
  8. import React, { ChangeEvent, useEffect, useState } from 'react'
  9. import { ExpandButton, ZhButton } from '../Button'
  10. import SvgIcon from '../SvgIcon'
  11. import './index.scss'
  12. interface iGroupItem extends iAccountGroupItem {
  13. onSelect: (item: iUserInfo) => void
  14. }
  15. export const GroupItem: React.FC<iGroupItem> = props => {
  16. const { onSelect } = props
  17. const [ visible, setVisible ] = useState<boolean>(true)
  18. const changeVisible = () => {
  19. setVisible(!visible)
  20. }
  21. return props.children.length ? (
  22. <div>
  23. <div className="group-item-label-name pi-align-center">
  24. <ExpandButton expanded={visible} onExpand={() => setVisible(!visible)}></ExpandButton>
  25. <span onClick={changeVisible} className="pi-mg-left-5">
  26. {props.value}
  27. </span>
  28. </div>
  29. <QueueAnim>
  30. {visible
  31. ? props.children.map((account: iUserInfo) => {
  32. return (
  33. <div key={account.id} className={[ 'group-item-content', 'pi-justify-between', 'item-border-bottom' ].join(' ')} onClick={() => onSelect(account)}>
  34. <div className="pi-flex-column">
  35. <span className="pi-blue">{account.name}</span>
  36. <span className="pi-gray">{account.position}</span>
  37. </div>
  38. <span>{account.mobile}</span>
  39. </div>
  40. )
  41. })
  42. : null}
  43. </QueueAnim>
  44. </div>
  45. ) : null
  46. }
  47. interface iAuditContentProps {
  48. onSelect: (type: string, item: iUserInfo) => void
  49. onDelete: (id: string, progress: string) => void
  50. latest: iLatestAuditorState
  51. auditors: iAuditor[]
  52. auditHistory: iAuditHistoryState[][]
  53. status: number
  54. uName: string
  55. }
  56. const Index: React.FC<iAuditContentProps> = props => {
  57. const { onSelect, auditors, onDelete, status, auditHistory } = props
  58. const [ visible, setVisible ] = useState({
  59. check: false,
  60. reCheck: false
  61. })
  62. const showPopover = (type: string) => {
  63. setVisible({ ...visible, [type]: true })
  64. }
  65. const [ searchValue, setSearchValue ] = useState<string>('')
  66. const [ groups, setGroups ] = useState<Array<iAccountGroupItem>>([])
  67. useEffect(() => {
  68. if (visible.check || visible.reCheck) {
  69. initGroupList()
  70. }
  71. }, [ visible.check, visible.reCheck ])
  72. const initGroupList = async (serach?: string) => {
  73. const data = await getUserGroup(serach)
  74. setGroups(data)
  75. }
  76. const handleVisibleChange = (type: string, isShow: boolean) => {
  77. setVisible({ ...visible, [type]: isShow })
  78. }
  79. const search = (value: string) => {
  80. if (value != searchValue) {
  81. setSearchValue(value)
  82. initGroupList(value)
  83. }
  84. }
  85. const change = (e: ChangeEvent) => {
  86. e.persist()
  87. const target = e.target as HTMLTextAreaElement
  88. if (!target.value) {
  89. initGroupList()
  90. }
  91. }
  92. const itemSelectHandler = (type: string, item: iUserInfo) => {
  93. onSelect(type, item)
  94. setVisible({ ...visible, [type]: false })
  95. }
  96. const renderAuditorSelectItem = (type = 'check') => {
  97. return (
  98. <ul className="card-content">
  99. {type === 'check'
  100. ? auditors
  101. .filter(item => item.progress === '0')
  102. .map((item, idx) => {
  103. return (
  104. <li key={item.audit_id}>
  105. <div className="pi-flex-column">
  106. <p>
  107. <span>{idx + 1}</span> {item.name}
  108. <small className="text-muted pi-mg-left-5">{item.position}</small>
  109. </p>
  110. <small className="text-muted">{item.company}</small>
  111. </div>
  112. <span className="pi-red pi-pointer" onClick={() => onDelete(item.audit_id, '0')}>
  113. 移除
  114. </span>
  115. </li>
  116. )
  117. })
  118. : auditors
  119. .filter(item => item.progress === '2')
  120. .map((item, idx) => {
  121. return (
  122. <li key={item.audit_id}>
  123. <div className="pi-flex-column">
  124. <p>
  125. <span>{idx + 1}</span> {item.name}
  126. <small className="text-muted pi-mg-left-5">{item.position}</small>
  127. </p>
  128. <small className="text-muted">{item.company}</small>
  129. </div>
  130. <span className="pi-red pi-pointer" onClick={() => onDelete(item.audit_id, '2')}>
  131. 移除
  132. </span>
  133. </li>
  134. )
  135. })}
  136. </ul>
  137. )
  138. }
  139. const renderStatusIcon = (status: number, isEnd: boolean) => {
  140. let bgColor: string = ''
  141. let iconType: string = ''
  142. switch (status) {
  143. case 0:
  144. bgColor = 'pi-bg-green'
  145. iconType = 'xxh-check'
  146. break
  147. case 1:
  148. bgColor = 'pi-bg-yellow'
  149. iconType = 'xxh-reply'
  150. break
  151. case 2:
  152. bgColor = 'pi-bg-red'
  153. iconType = 'xxh-minus'
  154. break
  155. default:
  156. break
  157. }
  158. if (isEnd) {
  159. iconType = 'xxh-caret-down1'
  160. }
  161. return <div className={[ 'timeline-item-icon', 'pi-justify-center', 'pi-align-center', bgColor ].join(' ')}>{iconType ? <SvgIcon type={iconType}></SvgIcon> : null}</div>
  162. }
  163. const renderStatusEle = (status: number, progress: string) => {
  164. let text = ''
  165. let textClass = 'pi-green'
  166. if (progress === '0') {
  167. switch (status) {
  168. case 0:
  169. text = '上报审批'
  170. textClass = 'pi-green'
  171. break
  172. case 1:
  173. text = '审批退回'
  174. textClass = 'pi-yellow'
  175. break
  176. case 2:
  177. text = '关闭'
  178. textClass = 'pi-red'
  179. break
  180. default:
  181. break
  182. }
  183. } else if (progress === '1' || progress === '3') {
  184. switch (status) {
  185. case 0:
  186. text = '审批通过'
  187. textClass = 'pi-green'
  188. break
  189. case 1:
  190. text = '审批退回'
  191. textClass = 'pi-yellow'
  192. break
  193. case 2:
  194. text = '关闭'
  195. textClass = 'pi-red'
  196. break
  197. default:
  198. break
  199. }
  200. } else if (progress === '2') {
  201. switch (status) {
  202. case 0:
  203. text = '整改完成'
  204. textClass = 'pi-green'
  205. break
  206. case 1:
  207. text = '审批退回'
  208. textClass = 'pi-yellow'
  209. break
  210. case 2:
  211. text = '关闭'
  212. textClass = 'pi-red'
  213. break
  214. default:
  215. break
  216. }
  217. }
  218. return { text, textClass }
  219. }
  220. const renderLeftStatus = (status: number) => {
  221. let text = ''
  222. let textClass = ''
  223. switch (status) {
  224. case 1:
  225. text = '进行中'
  226. textClass = 'pi-yellow'
  227. break
  228. case 2:
  229. text = '完成'
  230. textClass = 'pi-green'
  231. break
  232. case 3:
  233. text = '关闭'
  234. textClass = 'pi-red'
  235. break
  236. default:
  237. break
  238. }
  239. return { text, textClass }
  240. }
  241. const renderLeftAuditors = (status: number) => {
  242. // 整改人所需信息
  243. const len = auditors.filter(item => item.progress === '0').length
  244. return (
  245. <tbody>
  246. <tr>
  247. <td className="pi-text-center">检查人</td>
  248. <td>
  249. <SvgIcon type="xxh-play-circle"></SvgIcon>
  250. <span className="pi-mg-left-3">{auditors[0]?.name}</span>
  251. <small className="text-muted pi-mg-left-3">{auditors[0]?.position}</small>
  252. </td>
  253. {status !== auditConsts.uncheck ? (
  254. <td>
  255. <span className={renderLeftStatus(auditors.length > 1 ? 2 : 0).textClass}>{renderLeftStatus(auditors.length > 1 ? 2 : 0).text}</span>
  256. </td>
  257. ) : null}
  258. </tr>
  259. {auditors
  260. .filter(item => item.progress === '0')
  261. .map((item, idx) => {
  262. return idx === 0 ? (
  263. <tr key={item.audit_id}>
  264. <td rowSpan={auditors.filter(item => item.progress === '0').length} className="pi-text-center">
  265. 审批
  266. </td>
  267. <td>
  268. <SvgIcon type={item.status === 0 ? 'xxh-stop-circle' : 'xxh-chevron-circle-down'}></SvgIcon>
  269. <span className="pi-mg-left-3">{item.name}</span>
  270. <small className="text-muted pi-mg-left-3">{item.position}</small>
  271. </td>
  272. {status !== auditConsts.uncheck ? (
  273. <td>
  274. <span className={renderLeftStatus(item.status).textClass}>{renderLeftStatus(item.status).text}</span>
  275. </td>
  276. ) : null}
  277. </tr>
  278. ) : (
  279. <tr key={item.audit_id}>
  280. <td>
  281. <SvgIcon type={item.status === 0 ? 'xxh-stop-circle' : 'xxh-chevron-circle-down'}></SvgIcon>
  282. <span className="pi-mg-left-3">{item.name}</span>
  283. <small className="text-muted pi-mg-left-3">{item.position}</small>
  284. </td>
  285. {status !== auditConsts.uncheck ? (
  286. <td>
  287. <span className={renderLeftStatus(item.status).textClass}>{renderLeftStatus(item.status).text}</span>
  288. </td>
  289. ) : null}
  290. </tr>
  291. )
  292. })}
  293. <tr>
  294. <td className="pi-text-center">整改人</td>
  295. <td>
  296. <SvgIcon type={status === 0 ? 'xxh-stop-circle' : 'xxh-chevron-circle-down'}></SvgIcon>
  297. {status ? (
  298. auditors.find(item => item.progress === '1') ? (
  299. <>
  300. <span className="pi-mg-left-3">{auditors.find(item => item.progress === '1')?.name}</span>
  301. <small className="text-muted pi-mg-left-3">{auditors.find(item => item.progress === '1')?.position}</small>
  302. </>
  303. ) : (
  304. <span className="pi-mg-left-3">由 {auditors.filter(item => item.progress === '0')[len - 1]?.name} 指派</span>
  305. )
  306. ) : (
  307. <span className="pi-mg-left-3">
  308. <span className="pi-mg-3">
  309. {auditors.filter(item => item.progress === '0').length ? auditors.filter(item => item.progress === '0')[len - 1]?.name : '最后一个审批人'}
  310. </span>
  311. 指派
  312. </span>
  313. )}
  314. </td>
  315. {status !== auditConsts.uncheck ? (
  316. <td>
  317. <span className={renderLeftStatus(auditors.find(item => item.progress === '1')?.status || 0).textClass}>
  318. {renderLeftStatus(auditors.find(item => item.progress === '1')?.status || 0).text}
  319. </span>
  320. </td>
  321. ) : null}
  322. </tr>
  323. {auditors
  324. .filter(item => item.progress === '2')
  325. .map((item, idx) => {
  326. return idx === 0 ? (
  327. <tr key={item.audit_id}>
  328. <td rowSpan={auditors.filter(item => item.progress === '2').length} className="pi-text-center">
  329. 复查
  330. </td>
  331. <td>
  332. <SvgIcon type={item.status === 0 ? 'xxh-stop-circle' : 'xxh-chevron-circle-down'}></SvgIcon>
  333. <span className="pi-mg-left-3">{item.name}</span>
  334. <small className="text-muted pi-mg-left-3">{item.position}</small>
  335. </td>
  336. {status !== auditConsts.uncheck ? (
  337. <td>
  338. <span className={renderLeftStatus(item.status).textClass}>{renderLeftStatus(item.status).text}</span>
  339. </td>
  340. ) : null}
  341. </tr>
  342. ) : (
  343. <tr key={item.audit_id}>
  344. <td>
  345. <SvgIcon type={item.status === 0 ? 'xxh-stop-circle' : 'xxh-chevron-circle-down'}></SvgIcon>
  346. <span className="pi-mg-left-3">{item.name}</span>
  347. <small className="text-muted pi-mg-left-3">{item.position}</small>
  348. </td>
  349. {status !== auditConsts.uncheck ? (
  350. <td>
  351. <span className={renderLeftStatus(item.status).textClass}>{renderLeftStatus(item.status).text}</span>
  352. </td>
  353. ) : null}
  354. </tr>
  355. )
  356. })}
  357. </tbody>
  358. )
  359. }
  360. const renderHistory = () => {
  361. return (
  362. <>
  363. {auditHistory?.map((item, index) => {
  364. return (
  365. <ul className="timeline-list" key={index}>
  366. {item?.map((auditor, idx) => (
  367. <div key={idx}>
  368. <li className="timeline-list-item">
  369. <div
  370. className="timeline-item-date pi-flex-column"
  371. dangerouslySetInnerHTML={{
  372. __html: formatDate(auditor.create_time)
  373. }}></div>
  374. <div className={item.length - 1 === idx ? '' : 'timeline-item-tail'}></div>
  375. {renderStatusIcon(auditor.status, idx === item.length - 1)}
  376. <div className="timeline-item-content">
  377. <div className="card-container">
  378. <div className="card-content">
  379. <div className="pi-justify-between label">
  380. <span>{auditor.name}</span>
  381. <span className={renderStatusEle(auditor.status, auditor.progress).textClass}>{renderStatusEle(auditor.status, auditor.progress).text}</span>
  382. </div>
  383. <div className="text-muted">{auditor.position}</div>
  384. </div>
  385. {auditor.opinion ? (
  386. <div className="textarea">
  387. <span>{auditor.opinion}</span>
  388. </div>
  389. ) : null}
  390. </div>
  391. </div>
  392. </li>
  393. </div>
  394. ))}
  395. </ul>
  396. )
  397. })}
  398. </>
  399. )
  400. }
  401. return (
  402. <table className="pi-table pi-bordered mt-3">
  403. <thead>
  404. <tr>
  405. <th colSpan={2} className="pi-text-center">
  406. 审批流程
  407. </th>
  408. </tr>
  409. </thead>
  410. <tbody>
  411. <tr>
  412. {!status ? (
  413. <>
  414. <td width="30%">
  415. <table className="table table-bordered">{renderLeftAuditors(status)}</table>
  416. </td>
  417. <td width="70%">
  418. <div className="pi-justify-end">
  419. <Popover
  420. title={<Input.Search size="small" placeholder="姓名/手机 检索" onSearch={search} onChange={e => change(e)}></Input.Search>}
  421. content={groups.map(item => (
  422. <GroupItem {...item} key={item.value} onSelect={(item: iUserInfo) => itemSelectHandler('check', item)}></GroupItem>
  423. ))}
  424. overlayClassName="popover-card"
  425. trigger="click"
  426. visible={visible.check}
  427. onVisibleChange={visible => handleVisibleChange('check', visible)}
  428. placement="bottomRight">
  429. <ZhButton size="small" onClick={() => showPopover('check')}>
  430. 添加审批流程
  431. </ZhButton>
  432. </Popover>
  433. </div>
  434. <div className="audit-card">
  435. <div className="card-header">审批流程</div>
  436. {renderAuditorSelectItem('check')}
  437. </div>
  438. <div className="audit-card">
  439. <div className="card-header">整改流程</div>
  440. <ul className="card-content">
  441. <li>
  442. <span>整改人由最后一位审批人指派</span>
  443. </li>
  444. </ul>
  445. </div>
  446. <div className="pi-justify-end">
  447. <Popover
  448. title={<Input.Search size="small" placeholder="姓名/手机 检索" onSearch={search} onChange={e => change(e)}></Input.Search>}
  449. content={groups.map(item => (
  450. <GroupItem {...item} key={item.value} onSelect={(item: iUserInfo) => itemSelectHandler('reCheck', item)}></GroupItem>
  451. ))}
  452. overlayClassName="popover-card"
  453. trigger="click"
  454. visible={visible.reCheck}
  455. onVisibleChange={visible => handleVisibleChange('reCheck', visible)}
  456. placement="bottomRight">
  457. <ZhButton size="small" onClick={() => showPopover('reCheck')}>
  458. 添加审批流程
  459. </ZhButton>
  460. </Popover>
  461. </div>
  462. <div className="audit-card">
  463. <div className="card-header">复查流程</div>
  464. {renderAuditorSelectItem('reCheck')}
  465. </div>
  466. </td>
  467. </>
  468. ) : (
  469. <td>
  470. <div className="pi-flex-row">
  471. <div className="pi-flex-sub pi-mg-right-15 pi-mg-top-5 pi-mg-left-5">
  472. <table className="table table-bordered pi-width-100P">{renderLeftAuditors(status)}</table>
  473. </div>
  474. <div className="pi-flex-twice pi-mg-left-15">{renderHistory()}</div>
  475. </div>
  476. </td>
  477. )}
  478. </tr>
  479. </tbody>
  480. </table>
  481. )
  482. }
  483. export default Index