personnel-qualification.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. import type { TransformList, TransformRecord } from '#/api';
  2. /** 岗位人员资质(接口就绪后替换为真实请求) */
  3. export namespace PersonnelQualificationModel {
  4. export type QualificationStatus = 'valid' | 'expiring' | 'expired';
  5. export interface CertificateAttachment {
  6. id: string;
  7. name: string;
  8. url: string;
  9. type: 'image' | 'pdf';
  10. }
  11. export interface Certificate {
  12. id: string;
  13. name: string;
  14. type?: string;
  15. number: string;
  16. expiryDate: string;
  17. longTerm?: boolean;
  18. status: QualificationStatus;
  19. attachments: CertificateAttachment[];
  20. }
  21. export type OrganizationType = 'enterprise' | 'medicalInstitution';
  22. export interface Personnel extends TransformRecord {
  23. organizationType?: OrganizationType;
  24. enterpriseId?: string;
  25. enterpriseName: string;
  26. decoctionCenterId?: string;
  27. decoctionCenterName: string;
  28. name: string;
  29. positions: string[];
  30. employeeNo: string;
  31. idNumber: string;
  32. certificateNames: string;
  33. qualificationStatus: QualificationStatus;
  34. certificates: Certificate[];
  35. }
  36. export interface PersonnelForm {
  37. id?: string;
  38. decoctionCenterId: string;
  39. decoctionCenterName?: string;
  40. name: string;
  41. positions: string[];
  42. employeeNo: string;
  43. idNumber: string;
  44. certificates: Certificate[];
  45. }
  46. export interface ListQuery {
  47. organizationType?: OrganizationType;
  48. organizationId?: string;
  49. enterpriseId?: string;
  50. decoctionCenterId?: string;
  51. position?: string;
  52. qualificationStatus?: QualificationStatus;
  53. keyword?: string;
  54. }
  55. export interface ExpirySummary {
  56. expiredCount: number;
  57. expiringCount: number;
  58. }
  59. }
  60. export const POSITION_OPTIONS = [
  61. { label: '接方、审方', value: '接方、审方' },
  62. { label: '审方', value: '审方' },
  63. { label: '配药', value: '配药' },
  64. { label: '复核', value: '复核' },
  65. { label: '煎煮', value: '煎煮' },
  66. { label: '打包', value: '打包' },
  67. ];
  68. export const CERTIFICATE_NAME_OPTIONS = [
  69. { label: '健康证', value: '健康证' },
  70. { label: '中药调剂员', value: '中药调剂员' },
  71. { label: '中药保管员', value: '中药保管员' },
  72. { label: '执业中药师', value: '执业中药师' },
  73. ];
  74. export const QUALIFICATION_STATUS_OPTIONS = [
  75. { label: '有效', value: 'valid' },
  76. { label: '即将过期', value: 'expiring' },
  77. { label: '过期', value: 'expired' },
  78. ];
  79. export const ORGANIZATION_TYPE_OPTIONS: Array<{
  80. label: string;
  81. value: PersonnelQualificationModel.OrganizationType;
  82. }> = [
  83. { label: '煎药企业', value: 'enterprise' },
  84. { label: '医疗机构', value: 'medicalInstitution' },
  85. ];
  86. export const QUALIFICATION_STATUS_LABELS: Record<
  87. PersonnelQualificationModel.QualificationStatus,
  88. string
  89. > = {
  90. valid: '有效',
  91. expiring: '即将过期',
  92. expired: '过期',
  93. };
  94. /** 各证书类型独立的 mock 附件,避免多证共用同一套图片 */
  95. function getDefaultAttachments(
  96. certName: string,
  97. index: number,
  98. ): PersonnelQualificationModel.CertificateAttachment[] {
  99. const attachmentSets: Record<
  100. string,
  101. PersonnelQualificationModel.CertificateAttachment[]
  102. > = {
  103. 健康证: [
  104. {
  105. id: `health-${index}-1`,
  106. name: '健康证正面',
  107. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-8.jpeg',
  108. type: 'image',
  109. },
  110. {
  111. id: `health-${index}-2`,
  112. name: '健康证反面',
  113. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-7.jpeg',
  114. type: 'image',
  115. },
  116. ],
  117. 中药保管员: [
  118. {
  119. id: `custodian-${index}-1`,
  120. name: '保管员资格证',
  121. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-6.jpeg',
  122. type: 'image',
  123. },
  124. {
  125. id: `custodian-${index}-2`,
  126. name: '保管员证书扫描件',
  127. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-5.jpeg',
  128. type: 'image',
  129. },
  130. ],
  131. 执业中药师: [
  132. {
  133. id: `pharmacist-${index}-1`,
  134. name: '执业药师资格证',
  135. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-4.jpeg',
  136. type: 'image',
  137. },
  138. {
  139. id: `pharmacist-${index}-2`,
  140. name: '执业药师注册证',
  141. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-3.jpeg',
  142. type: 'pdf',
  143. },
  144. ],
  145. 中药调剂员: [
  146. {
  147. id: `dispenser-${index}-1`,
  148. name: '调剂员资格证',
  149. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-2.jpeg',
  150. type: 'image',
  151. },
  152. {
  153. id: `dispenser-${index}-2`,
  154. name: '调剂员证书扫描件',
  155. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-1.jpeg',
  156. type: 'image',
  157. },
  158. ],
  159. };
  160. return (
  161. attachmentSets[certName] ?? [
  162. {
  163. id: `cert-${index}-1`,
  164. name: '证书附件',
  165. url: 'https://fastly.jsdelivr.net/npm/@vant/assets/apple-8.jpeg',
  166. type: 'image',
  167. },
  168. ]
  169. );
  170. }
  171. function buildCertificates(
  172. items: Array<
  173. Omit<PersonnelQualificationModel.Certificate, 'id' | 'attachments'> & {
  174. attachments?: PersonnelQualificationModel.CertificateAttachment[];
  175. }
  176. >,
  177. ): PersonnelQualificationModel.Certificate[] {
  178. return items.map((item, index) => ({
  179. ...item,
  180. id: `cert-${index + 1}`,
  181. attachments: item.attachments ?? getDefaultAttachments(item.name, index),
  182. }));
  183. }
  184. function resolveWorstStatus(
  185. certificates: PersonnelQualificationModel.Certificate[],
  186. ): PersonnelQualificationModel.QualificationStatus {
  187. if (certificates.some((item) => item.status === 'expired')) return 'expired';
  188. if (certificates.some((item) => item.status === 'expiring')) return 'expiring';
  189. return 'valid';
  190. }
  191. function buildPersonnel(
  192. data: Omit<
  193. PersonnelQualificationModel.Personnel,
  194. 'certificateNames' | 'qualificationStatus'
  195. >,
  196. ): PersonnelQualificationModel.Personnel {
  197. const certificateNames = data.certificates.map((item) => item.name).join('、');
  198. return {
  199. ...data,
  200. certificateNames,
  201. qualificationStatus: resolveWorstStatus(data.certificates),
  202. };
  203. }
  204. const MOCK_PERSONNEL: PersonnelQualificationModel.Personnel[] = [
  205. buildPersonnel({
  206. id: '1',
  207. organizationType: 'enterprise',
  208. enterpriseId: 'e1',
  209. enterpriseName: '重药煎药中心',
  210. decoctionCenterId: 'c1',
  211. decoctionCenterName: '重药华东煎药中心2',
  212. name: '孙明1',
  213. positions: ['接方、审方'],
  214. employeeNo: '28473',
  215. idNumber: '330102199001012839',
  216. certificates: buildCertificates([
  217. {
  218. name: '健康证',
  219. number: '944895756806594342',
  220. expiryDate: '2027-3-20',
  221. status: 'valid',
  222. type: '健康证',
  223. },
  224. {
  225. name: '中药保管员',
  226. number: '944895756806594343',
  227. expiryDate: '2026-4-15',
  228. status: 'expiring',
  229. type: '保管员',
  230. },
  231. {
  232. name: '执业中药师',
  233. number: '944895756806594344',
  234. expiryDate: '2028-6-30',
  235. status: 'valid',
  236. type: '执业药师',
  237. },
  238. ]),
  239. }),
  240. buildPersonnel({
  241. id: '2',
  242. enterpriseId: 'e1',
  243. enterpriseName: '重药煎药中心',
  244. decoctionCenterId: 'c1',
  245. decoctionCenterName: '重药华东煎药中心2',
  246. name: '孙明2',
  247. positions: ['审方'],
  248. employeeNo: '28474',
  249. idNumber: '330103198802022840',
  250. certificates: buildCertificates([
  251. {
  252. name: '健康证',
  253. number: '944895756806594345',
  254. expiryDate: '2025-1-10',
  255. status: 'expired',
  256. },
  257. {
  258. name: '执业中药师',
  259. number: '944895756806594346',
  260. expiryDate: '2027-8-20',
  261. status: 'valid',
  262. },
  263. ]),
  264. }),
  265. buildPersonnel({
  266. id: '3',
  267. enterpriseId: 'e1',
  268. enterpriseName: '重药煎药中心',
  269. decoctionCenterId: 'c1',
  270. decoctionCenterName: '重药华东煎药中心2',
  271. name: '孙明3',
  272. positions: ['配药'],
  273. employeeNo: '28475',
  274. idNumber: '330104199203033841',
  275. certificates: buildCertificates([
  276. {
  277. name: '中药调剂员',
  278. number: '944895756806594347',
  279. expiryDate: '2026-5-1',
  280. status: 'expiring',
  281. },
  282. {
  283. name: '健康证',
  284. number: '944895756806594348',
  285. expiryDate: '2027-2-18',
  286. status: 'valid',
  287. },
  288. ]),
  289. }),
  290. buildPersonnel({
  291. id: '4',
  292. enterpriseId: 'e1',
  293. enterpriseName: '重药煎药中心',
  294. decoctionCenterId: 'c1',
  295. decoctionCenterName: '重药华东煎药中心2',
  296. name: '孙明4',
  297. positions: ['复核'],
  298. employeeNo: '28476',
  299. idNumber: '330105199504044842',
  300. certificates: buildCertificates([
  301. {
  302. name: '健康证',
  303. number: '944895756806594349',
  304. expiryDate: '2027-12-31',
  305. status: 'valid',
  306. },
  307. ]),
  308. }),
  309. buildPersonnel({
  310. id: '5',
  311. enterpriseId: 'e1',
  312. enterpriseName: '重药煎药中心',
  313. decoctionCenterId: 'c1',
  314. decoctionCenterName: '重药华东煎药中心2',
  315. name: '孙明5',
  316. positions: ['煎煮'],
  317. employeeNo: '28477',
  318. idNumber: '330106199605055843',
  319. certificates: buildCertificates([
  320. {
  321. name: '健康证',
  322. number: '944895756806594350',
  323. expiryDate: '2024-12-1',
  324. status: 'expired',
  325. },
  326. ]),
  327. }),
  328. buildPersonnel({
  329. id: '6',
  330. enterpriseId: 'e1',
  331. enterpriseName: '重药煎药中心',
  332. decoctionCenterId: 'c1',
  333. decoctionCenterName: '重药华东煎药中心2',
  334. name: '孙明6',
  335. positions: ['打包'],
  336. employeeNo: '28478',
  337. idNumber: '330107199706066844',
  338. certificates: buildCertificates([
  339. {
  340. name: '健康证',
  341. number: '944895756806594351',
  342. expiryDate: '2027-9-10',
  343. status: 'valid',
  344. },
  345. ]),
  346. }),
  347. buildPersonnel({
  348. id: '7',
  349. enterpriseId: 'e2',
  350. enterpriseName: '杭州中药煎配中心',
  351. decoctionCenterId: 'c2',
  352. decoctionCenterName: '西湖煎药中心',
  353. name: '李明1',
  354. positions: ['接方、审方'],
  355. employeeNo: '38473',
  356. idNumber: '330108199807077845',
  357. certificates: buildCertificates([
  358. {
  359. name: '执业中药师',
  360. number: '944895756806594352',
  361. expiryDate: '2026-3-20',
  362. status: 'expiring',
  363. },
  364. ]),
  365. }),
  366. buildPersonnel({
  367. id: '8',
  368. organizationType: 'enterprise',
  369. enterpriseId: 'e2',
  370. enterpriseName: '杭州中药煎配中心',
  371. decoctionCenterId: 'c2',
  372. decoctionCenterName: '西湖煎药中心',
  373. name: '李明2',
  374. positions: ['配药'],
  375. employeeNo: '38474',
  376. idNumber: '330109199908088846',
  377. certificates: buildCertificates([
  378. {
  379. name: '中药调剂员',
  380. number: '944895756806594353',
  381. expiryDate: '2027-6-15',
  382. status: 'valid',
  383. },
  384. ]),
  385. }),
  386. buildPersonnel({
  387. id: '9',
  388. organizationType: 'medicalInstitution',
  389. enterpriseId: 'm1',
  390. enterpriseName: '蒋村社区卫生服务中心',
  391. decoctionCenterId: 'c3',
  392. decoctionCenterName: '蒋村煎药中心',
  393. name: '王芳',
  394. positions: ['接方、审方'],
  395. employeeNo: '48473',
  396. idNumber: '330110199001011234',
  397. certificates: buildCertificates([
  398. {
  399. name: '健康证',
  400. number: '944895756806594360',
  401. expiryDate: '2027-5-20',
  402. status: 'valid',
  403. },
  404. {
  405. name: '执业中药师',
  406. number: '944895756806594361',
  407. expiryDate: '2026-2-10',
  408. status: 'expiring',
  409. },
  410. ]),
  411. }),
  412. buildPersonnel({
  413. id: '10',
  414. organizationType: 'medicalInstitution',
  415. enterpriseId: 'm2',
  416. enterpriseName: '西湖区中医院',
  417. decoctionCenterId: 'c4',
  418. decoctionCenterName: '西湖区中医院煎药室',
  419. name: '赵强',
  420. positions: ['配药'],
  421. employeeNo: '48474',
  422. idNumber: '330111198803022345',
  423. certificates: buildCertificates([
  424. {
  425. name: '中药调剂员',
  426. number: '944895756806594362',
  427. expiryDate: '2027-8-1',
  428. status: 'valid',
  429. },
  430. ]),
  431. }),
  432. ];
  433. let mockStore = [...MOCK_PERSONNEL];
  434. function filterPersonnel(
  435. list: PersonnelQualificationModel.Personnel[],
  436. query?: PersonnelQualificationModel.ListQuery,
  437. ) {
  438. if (!query) return list;
  439. return list.filter((item) => {
  440. const organizationId = query.organizationId ?? query.enterpriseId;
  441. const organizationType = query.organizationType ?? 'enterprise';
  442. if (organizationId) {
  443. const itemType = item.organizationType ?? 'enterprise';
  444. if (itemType !== organizationType) {
  445. return false;
  446. }
  447. if (item.enterpriseId !== organizationId) {
  448. return false;
  449. }
  450. }
  451. if (
  452. query.decoctionCenterId &&
  453. item.decoctionCenterId !== query.decoctionCenterId
  454. ) {
  455. return false;
  456. }
  457. if (query.position && !item.positions.includes(query.position)) {
  458. return false;
  459. }
  460. if (
  461. query.qualificationStatus &&
  462. item.qualificationStatus !== query.qualificationStatus
  463. ) {
  464. return false;
  465. }
  466. if (query.keyword) {
  467. const keyword = query.keyword.trim();
  468. if (
  469. !item.name.includes(keyword) &&
  470. !item.employeeNo.includes(keyword)
  471. ) {
  472. return false;
  473. }
  474. }
  475. return true;
  476. });
  477. }
  478. function computeExpirySummary(): PersonnelQualificationModel.ExpirySummary {
  479. let expiredCount = 0;
  480. let expiringCount = 0;
  481. for (const person of mockStore) {
  482. if (person.qualificationStatus === 'expired') expiredCount += 1;
  483. else if (person.qualificationStatus === 'expiring') expiringCount += 1;
  484. }
  485. return { expiredCount, expiringCount };
  486. }
  487. /** 岗位人员资质列表(当前为本地 mock,后期对接后端接口) */
  488. export function listPersonnelQualificationsMethod(
  489. page = 1,
  490. size = 10,
  491. query?: PersonnelQualificationModel.ListQuery,
  492. ): Promise<TransformList<PersonnelQualificationModel.Personnel>> {
  493. const filtered = filterPersonnel(mockStore, query);
  494. const start = (page - 1) * size;
  495. const items = filtered.slice(start, start + size);
  496. return Promise.resolve({
  497. items,
  498. total: filtered.length,
  499. data: { page, size, total: filtered.length },
  500. });
  501. }
  502. /** 资质到期统计(当前为本地 mock) */
  503. export function getPersonnelQualificationSummaryMethod(): Promise<PersonnelQualificationModel.ExpirySummary> {
  504. return Promise.resolve(computeExpirySummary());
  505. }
  506. /** 岗位人员资质详情(当前为本地 mock) */
  507. export function getPersonnelQualificationMethod(id: string) {
  508. const item = mockStore.find((row) => row.id === id);
  509. if (!item) {
  510. return Promise.reject(new Error('人员记录不存在'));
  511. }
  512. return Promise.resolve({ ...item });
  513. }
  514. /** 新增/修改岗位人员资质(当前为本地 mock) */
  515. export function editPersonnelQualificationMethod(
  516. data: PersonnelQualificationModel.PersonnelForm,
  517. ) {
  518. const certificates = data.certificates.map((cert, index) => ({
  519. ...cert,
  520. id: cert.id || `cert-${Date.now()}-${index}`,
  521. status: cert.status ?? 'valid',
  522. attachments: cert.attachments ?? [],
  523. }));
  524. const enterpriseName =
  525. mockStore.find((item) => item.decoctionCenterId === data.decoctionCenterId)
  526. ?.enterpriseName ?? '重药煎药中心';
  527. const decoctionCenterName =
  528. data.decoctionCenterName ??
  529. mockStore.find((item) => item.decoctionCenterId === data.decoctionCenterId)
  530. ?.decoctionCenterName ??
  531. '重药华东煎药中心2';
  532. const payload = buildPersonnel({
  533. id: data.id ?? String(Date.now()),
  534. enterpriseId: mockStore.find(
  535. (item) => item.decoctionCenterId === data.decoctionCenterId,
  536. )?.enterpriseId,
  537. enterpriseName,
  538. decoctionCenterId: data.decoctionCenterId,
  539. decoctionCenterName,
  540. name: data.name,
  541. positions: data.positions,
  542. employeeNo: data.employeeNo,
  543. idNumber: data.idNumber,
  544. certificates,
  545. });
  546. if (data.id) {
  547. const index = mockStore.findIndex((item) => item.id === data.id);
  548. if (index === -1) {
  549. return Promise.reject(new Error('人员记录不存在'));
  550. }
  551. mockStore[index] = payload;
  552. } else {
  553. mockStore = [payload, ...mockStore];
  554. }
  555. return Promise.resolve(payload);
  556. }
  557. /** 删除岗位人员资质(当前为本地 mock) */
  558. export function deletePersonnelQualificationMethod(id: string) {
  559. const index = mockStore.findIndex((item) => item.id === id);
  560. if (index === -1) {
  561. return Promise.reject(new Error('人员记录不存在'));
  562. }
  563. mockStore.splice(index, 1);
  564. return Promise.resolve(true);
  565. }
  566. /** 煎药企业选项(mock) */
  567. export function optionsPersonnelEnterpriseMethod() {
  568. const map = new Map<string, string>();
  569. for (const item of mockStore) {
  570. if ((item.organizationType ?? 'enterprise') !== 'enterprise') continue;
  571. if (item.enterpriseId) {
  572. map.set(item.enterpriseId, item.enterpriseName);
  573. }
  574. }
  575. return Promise.resolve(
  576. [...map.entries()].map(([value, label]) => ({ label, value })),
  577. );
  578. }
  579. /** 医疗机构选项(mock) */
  580. export function optionsPersonnelMedicalInstitutionMethod() {
  581. const map = new Map<string, string>();
  582. for (const item of mockStore) {
  583. if (item.organizationType !== 'medicalInstitution') continue;
  584. if (item.enterpriseId) {
  585. map.set(item.enterpriseId, item.enterpriseName);
  586. }
  587. }
  588. return Promise.resolve(
  589. [...map.entries()].map(([value, label]) => ({ label, value })),
  590. );
  591. }
  592. /** 煎药中心选项(mock) */
  593. export function optionsPersonnelDecoctionCenterMethod(
  594. organizationId?: string,
  595. organizationType: PersonnelQualificationModel.OrganizationType = 'enterprise',
  596. ) {
  597. const map = new Map<string, string>();
  598. for (const item of mockStore) {
  599. const itemType = item.organizationType ?? 'enterprise';
  600. if (itemType !== organizationType) continue;
  601. if (organizationId && item.enterpriseId !== organizationId) continue;
  602. if (item.decoctionCenterId) {
  603. map.set(item.decoctionCenterId, item.decoctionCenterName);
  604. }
  605. }
  606. return Promise.resolve(
  607. [...map.entries()].map(([value, label]) => ({ label, value })),
  608. );
  609. }