Ver código fonte

feat(@six/smart-pharmacy): 智慧药事系统第二版-岗位人员资质查询静态页面新增

cmj 3 semanas atrás
pai
commit
ac307cf13d

+ 10 - 0
apps/smart-pharmacy/public/database/menu.json

@@ -68,6 +68,16 @@
           "title": "岗位人员资质管理"
         },
         "component": "/system/personnel-qualification/list"
+      },
+      {
+        "id": "2416",
+        "path": "/system/personnel-qualification-query",
+        "name": "SystemPersonnelQualificationQuery",
+        "meta": {
+          "icon": "mdi:certificate",
+          "title": "岗位人员资质查询"
+        },
+        "component": "/system/personnel-qualification-query/list"
       }
     ]
   },

+ 92 - 4
apps/smart-pharmacy/src/api/method/personnel-qualification.ts

@@ -22,7 +22,10 @@ export namespace PersonnelQualificationModel {
     attachments: CertificateAttachment[];
   }
 
+  export type OrganizationType = 'enterprise' | 'medicalInstitution';
+
   export interface Personnel extends TransformRecord {
+    organizationType?: OrganizationType;
     enterpriseId?: string;
     enterpriseName: string;
     decoctionCenterId?: string;
@@ -48,6 +51,8 @@ export namespace PersonnelQualificationModel {
   }
 
   export interface ListQuery {
+    organizationType?: OrganizationType;
+    organizationId?: string;
     enterpriseId?: string;
     decoctionCenterId?: string;
     position?: string;
@@ -83,6 +88,14 @@ export const QUALIFICATION_STATUS_OPTIONS = [
   { label: '过期', value: 'expired' },
 ];
 
+export const ORGANIZATION_TYPE_OPTIONS: Array<{
+  label: string;
+  value: PersonnelQualificationModel.OrganizationType;
+}> = [
+  { label: '煎药企业', value: 'enterprise' },
+  { label: '医疗机构', value: 'medicalInstitution' },
+];
+
 export const QUALIFICATION_STATUS_LABELS: Record<
   PersonnelQualificationModel.QualificationStatus,
   string
@@ -210,6 +223,7 @@ function buildPersonnel(
 const MOCK_PERSONNEL: PersonnelQualificationModel.Personnel[] = [
   buildPersonnel({
     id: '1',
+    organizationType: 'enterprise',
     enterpriseId: 'e1',
     enterpriseName: '重药煎药中心',
     decoctionCenterId: 'c1',
@@ -370,6 +384,7 @@ const MOCK_PERSONNEL: PersonnelQualificationModel.Personnel[] = [
   }),
   buildPersonnel({
     id: '8',
+    organizationType: 'enterprise',
     enterpriseId: 'e2',
     enterpriseName: '杭州中药煎配中心',
     decoctionCenterId: 'c2',
@@ -387,6 +402,52 @@ const MOCK_PERSONNEL: PersonnelQualificationModel.Personnel[] = [
       },
     ]),
   }),
+  buildPersonnel({
+    id: '9',
+    organizationType: 'medicalInstitution',
+    enterpriseId: 'm1',
+    enterpriseName: '蒋村社区卫生服务中心',
+    decoctionCenterId: 'c3',
+    decoctionCenterName: '蒋村煎药中心',
+    name: '王芳',
+    positions: ['接方、审方'],
+    employeeNo: '48473',
+    idNumber: '330110199001011234',
+    certificates: buildCertificates([
+      {
+        name: '健康证',
+        number: '944895756806594360',
+        expiryDate: '2027-5-20',
+        status: 'valid',
+      },
+      {
+        name: '执业中药师',
+        number: '944895756806594361',
+        expiryDate: '2026-2-10',
+        status: 'expiring',
+      },
+    ]),
+  }),
+  buildPersonnel({
+    id: '10',
+    organizationType: 'medicalInstitution',
+    enterpriseId: 'm2',
+    enterpriseName: '西湖区中医院',
+    decoctionCenterId: 'c4',
+    decoctionCenterName: '西湖区中医院煎药室',
+    name: '赵强',
+    positions: ['配药'],
+    employeeNo: '48474',
+    idNumber: '330111198803022345',
+    certificates: buildCertificates([
+      {
+        name: '中药调剂员',
+        number: '944895756806594362',
+        expiryDate: '2027-8-1',
+        status: 'valid',
+      },
+    ]),
+  }),
 ];
 
 let mockStore = [...MOCK_PERSONNEL];
@@ -398,8 +459,17 @@ function filterPersonnel(
   if (!query) return list;
 
   return list.filter((item) => {
-    if (query.enterpriseId && item.enterpriseId !== query.enterpriseId) {
-      return false;
+    const organizationId = query.organizationId ?? query.enterpriseId;
+    const organizationType = query.organizationType ?? 'enterprise';
+
+    if (organizationId) {
+      const itemType = item.organizationType ?? 'enterprise';
+      if (itemType !== organizationType) {
+        return false;
+      }
+      if (item.enterpriseId !== organizationId) {
+        return false;
+      }
     }
     if (
       query.decoctionCenterId &&
@@ -533,6 +603,21 @@ export function deletePersonnelQualificationMethod(id: string) {
 export function optionsPersonnelEnterpriseMethod() {
   const map = new Map<string, string>();
   for (const item of mockStore) {
+    if ((item.organizationType ?? 'enterprise') !== 'enterprise') continue;
+    if (item.enterpriseId) {
+      map.set(item.enterpriseId, item.enterpriseName);
+    }
+  }
+  return Promise.resolve(
+    [...map.entries()].map(([value, label]) => ({ label, value })),
+  );
+}
+
+/** 医疗机构选项(mock) */
+export function optionsPersonnelMedicalInstitutionMethod() {
+  const map = new Map<string, string>();
+  for (const item of mockStore) {
+    if (item.organizationType !== 'medicalInstitution') continue;
     if (item.enterpriseId) {
       map.set(item.enterpriseId, item.enterpriseName);
     }
@@ -544,11 +629,14 @@ export function optionsPersonnelEnterpriseMethod() {
 
 /** 煎药中心选项(mock) */
 export function optionsPersonnelDecoctionCenterMethod(
-  enterpriseId?: string,
+  organizationId?: string,
+  organizationType: PersonnelQualificationModel.OrganizationType = 'enterprise',
 ) {
   const map = new Map<string, string>();
   for (const item of mockStore) {
-    if (enterpriseId && item.enterpriseId !== enterpriseId) continue;
+    const itemType = item.organizationType ?? 'enterprise';
+    if (itemType !== organizationType) continue;
+    if (organizationId && item.enterpriseId !== organizationId) continue;
     if (item.decoctionCenterId) {
       map.set(item.decoctionCenterId, item.decoctionCenterName);
     }

+ 5 - 2
apps/smart-pharmacy/src/api/model/menu.ts

@@ -39,7 +39,10 @@ export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
   {
     id: '1',
     label: '系统管理',
-    children: [{ id: '2415', label: '岗位人员资质管理' }],
+    children: [
+      { id: '2415', label: '岗位人员资质管理' },
+      { id: '2416', label: '岗位人员资质查询' },
+    ],
   },
   {
     id: '2500',
@@ -60,7 +63,7 @@ export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
 ];
 
 const HARDCODED_MENU_ROOT_IDS = ['2500', '2600'];
-const HARDCODED_MENU_LEAF_IDS = ['2415'];
+const HARDCODED_MENU_LEAF_IDS = ['2415', '2416'];
 
 /** 将本地写死菜单合并进后端 treeselect 结果 */
 export function mergeHardcodedMenuTree(

+ 112 - 0
apps/smart-pharmacy/src/views/system/personnel-qualification-query/list.vue

@@ -0,0 +1,112 @@
+<script lang="ts" setup>
+import type {
+  OnActionClickParams,
+  VxeTableGridOptions,
+} from '#/adapter/vxe-table';
+import type { PersonnelQualificationModel } from '#/api';
+
+import { onMounted, ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+  getPersonnelQualificationSummaryMethod,
+  listPersonnelQualificationsMethod,
+} from '#/api';
+
+import {
+  usePersonnelQualificationSearchFormSchema,
+  usePersonnelQualificationTableColumns,
+} from '../personnel-qualification/data';
+import Certificate from '../personnel-qualification/modules/certificate.vue';
+
+const summary = ref<PersonnelQualificationModel.ExpirySummary>({
+  expiredCount: 0,
+  expiringCount: 0,
+});
+
+const [CertificateModal, certificateModalApi] = useVbenModal({
+  connectedComponent: Certificate,
+  destroyOnClose: true,
+});
+
+const [Grid] = useVbenVxeGrid({
+  formOptions: {
+    schema: usePersonnelQualificationSearchFormSchema(),
+    submitOnChange: false,
+    wrapperClass:
+      'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 personnel-qualification-search-form',
+  },
+  gridOptions: {
+    columns: usePersonnelQualificationTableColumns(onActionClick, true),
+    height: 'auto',
+    keepSource: true,
+    pagerConfig: {
+      pageSize: 10,
+      layouts: ['PrevPage', 'Jump', 'NextPage'],
+    },
+    proxyConfig: {
+      ajax: {
+        query({ page }, formValues) {
+          return listPersonnelQualificationsMethod(
+            page.currentPage,
+            page.pageSize,
+            formValues,
+          );
+        },
+      },
+    },
+    rowConfig: {
+      keyField: 'id',
+    },
+    stripe: true,
+  } as VxeTableGridOptions<PersonnelQualificationModel.Personnel>,
+});
+
+async function loadSummary() {
+  summary.value = await getPersonnelQualificationSummaryMethod();
+}
+
+function onActionClick(
+  e: OnActionClickParams<PersonnelQualificationModel.Personnel>,
+) {
+  if (e.code === 'certificate') {
+    onCertificateHandle(e.row);
+  }
+}
+
+function onCertificateHandle(row: PersonnelQualificationModel.Personnel) {
+  certificateModalApi.setData(row).open();
+}
+
+onMounted(() => {
+  loadSummary();
+});
+</script>
+<template>
+  <Page auto-content-height class="personnel-qualification-page">
+    <CertificateModal />
+    <div
+      v-if="summary.expiredCount || summary.expiringCount"
+      class="qualification-alert"
+    >
+      有{{ summary.expiredCount }}位人员的资质证书已过期,有{{
+        summary.expiringCount
+      }}位人员的资质证书即将过期
+    </div>
+    <Grid />
+  </Page>
+</template>
+<style scoped>
+.personnel-qualification-page :deep(.personnel-qualification-search-form) {
+  margin-bottom: 0;
+}
+
+.qualification-alert {
+  margin: 0 0 12px;
+  color: #ff4d4f;
+  font-size: 14px;
+  line-height: 1.6;
+}
+</style>

+ 116 - 0
apps/smart-pharmacy/src/views/system/personnel-qualification/components/organization-entity-filter.vue

@@ -0,0 +1,116 @@
+<script lang="ts" setup>
+import type { SelectValue } from 'ant-design-vue/es/select';
+
+import type { PersonnelQualificationModel } from '#/api/method/personnel-qualification';
+
+import { computed, ref, watch } from 'vue';
+
+import { InputGroup, Select, Spin } from 'ant-design-vue';
+
+import {
+  ORGANIZATION_TYPE_OPTIONS,
+  optionsPersonnelEnterpriseMethod,
+  optionsPersonnelMedicalInstitutionMethod,
+} from '#/api/method/personnel-qualification';
+
+const props = withDefaults(
+  defineProps<{
+    organizationType?: PersonnelQualificationModel.OrganizationType;
+    value?: string;
+  }>(),
+  {
+    organizationType: 'enterprise',
+    value: undefined,
+  },
+);
+
+const emit = defineEmits<{
+  'update:organizationType': [PersonnelQualificationModel.OrganizationType];
+  'update:value': [string | undefined];
+}>();
+
+const loading = ref(false);
+const entityOptions = ref<Array<{ label: string; value: string }>>([]);
+
+const typeValue = computed({
+  get: () => props.organizationType,
+  set: (next) => emit('update:organizationType', next),
+});
+
+const entityValue = computed({
+  get: () => props.value,
+  set: (next) => emit('update:value', next || undefined),
+});
+
+async function loadEntityOptions(type: PersonnelQualificationModel.OrganizationType) {
+  loading.value = true;
+  try {
+    entityOptions.value =
+      type === 'medicalInstitution'
+        ? await optionsPersonnelMedicalInstitutionMethod()
+        : await optionsPersonnelEnterpriseMethod();
+  } finally {
+    loading.value = false;
+  }
+}
+
+watch(
+  () => props.organizationType,
+  (type) => {
+    loadEntityOptions(type);
+  },
+  { immediate: true },
+);
+
+function handleTypeChange(value: SelectValue) {
+  if (value !== 'enterprise' && value !== 'medicalInstitution') return;
+  typeValue.value = value;
+  entityValue.value = undefined;
+}
+</script>
+
+<template>
+  <InputGroup compact class="organization-entity-filter w-full">
+    <Select
+      :options="ORGANIZATION_TYPE_OPTIONS"
+      :value="typeValue"
+      class="organization-type-select"
+      @change="handleTypeChange"
+    />
+    <Select
+      v-model:value="entityValue"
+      :filter-option="
+        (input, option) =>
+          (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+      "
+      :loading="loading"
+      :not-found-content="loading ? undefined : null"
+      :options="entityOptions"
+      allow-clear
+      class="organization-entity-select"
+      option-filter-prop="label"
+      placeholder="请搜索选择"
+      show-search
+    >
+      <template v-if="loading" #notFoundContent>
+        <Spin size="small" />
+      </template>
+    </Select>
+  </InputGroup>
+</template>
+
+<style scoped>
+.organization-entity-filter {
+  display: flex;
+}
+
+.organization-type-select {
+  flex: 0 0 100px;
+  width: 100px !important;
+}
+
+.organization-entity-select {
+  flex: 1;
+  min-width: 0;
+}
+</style>

+ 58 - 31
apps/smart-pharmacy/src/views/system/personnel-qualification/data.ts

@@ -9,27 +9,38 @@ import { h } from 'vue';
 import {
   CERTIFICATE_NAME_OPTIONS,
   optionsPersonnelDecoctionCenterMethod,
-  optionsPersonnelEnterpriseMethod,
   POSITION_OPTIONS,
   QUALIFICATION_STATUS_LABELS,
   QUALIFICATION_STATUS_OPTIONS,
 } from '#/api/method/personnel-qualification';
 import { maskIdNumber } from '#/utils/mask-id';
 
+import OrganizationEntityFilter from './components/organization-entity-filter.vue';
+
 export function usePersonnelQualificationSearchFormSchema(): VbenFormSchema[] {
   return [
     {
-      component: 'ApiSelect',
-      componentProps: {
-        allowClear: true,
-        api: optionsPersonnelEnterpriseMethod,
-        class: 'w-full',
-        labelField: 'label',
-        showSearch: true,
-        valueField: 'value',
+      component: OrganizationEntityFilter,
+      defaultValue: undefined,
+      dependencies: {
+        componentProps: (values, formApi) => ({
+          organizationType: values.organizationType ?? 'enterprise',
+          'onUpdate:organizationType': (
+            type: 'enterprise' | 'medicalInstitution',
+          ) => {
+            formApi.setFieldValue('organizationType', type);
+            formApi.setFieldValue('organizationId', undefined);
+            formApi.setFieldValue('decoctionCenterId', undefined);
+          },
+        }),
+        trigger: (_values, formApi) => {
+          formApi.setFieldValue('decoctionCenterId', undefined);
+        },
+        triggerFields: ['organizationId', 'organizationType'],
       },
-      fieldName: 'enterpriseId',
-      label: '煎药企业',
+      fieldName: 'organizationId',
+      hideLabel: true,
+      modelPropName: 'value',
     },
     {
       component: 'ApiSelect',
@@ -42,20 +53,27 @@ export function usePersonnelQualificationSearchFormSchema(): VbenFormSchema[] {
         valueField: 'value',
       },
       dependencies: {
-        componentProps: (values) => ({
-          key: `decoction-center-${values.enterpriseId || 'all'}`,
-          allowClear: true,
-          api: () =>
-            optionsPersonnelDecoctionCenterMethod(values.enterpriseId),
-          class: 'w-full',
-          labelField: 'label',
-          showSearch: true,
-          valueField: 'value',
-        }),
+        componentProps: (values) => {
+          const organizationType = values.organizationType ?? 'enterprise';
+          const organizationId = values.organizationId;
+          return {
+            key: `decoction-center-${organizationType}-${organizationId || 'all'}`,
+            allowClear: true,
+            api: () =>
+              optionsPersonnelDecoctionCenterMethod(
+                organizationId,
+                organizationType,
+              ),
+            class: 'w-full',
+            labelField: 'label',
+            showSearch: true,
+            valueField: 'value',
+          };
+        },
         trigger: (_values, formApi) => {
           formApi.setFieldValue('decoctionCenterId', undefined);
         },
-        triggerFields: ['enterpriseId'],
+        triggerFields: ['organizationId', 'organizationType'],
       },
       fieldName: 'decoctionCenterId',
       label: '煎药中心',
@@ -93,6 +111,7 @@ export function usePersonnelQualificationSearchFormSchema(): VbenFormSchema[] {
 
 export function usePersonnelQualificationTableColumns(
   onActionClick?: OnActionClickFn<PersonnelQualificationModel.Personnel>,
+  readonly = false,
 ): VxeTableGridOptions<PersonnelQualificationModel.Personnel>['columns'] {
   return [
     {
@@ -164,15 +183,23 @@ export function usePersonnelQualificationTableColumns(
           onClick: onActionClick,
         },
         name: 'CellOperation',
-        options: [
-          {
-            code: 'certificate',
-            style: { color: '#fa8c16' },
-            text: '证书',
-          },
-          'edit',
-          'delete',
-        ],
+        options: readonly
+          ? [
+              {
+                code: 'certificate',
+                style: { color: '#fa8c16' },
+                text: '证书',
+              },
+            ]
+          : [
+              {
+                code: 'certificate',
+                style: { color: '#fa8c16' },
+                text: '证书',
+              },
+              'edit',
+              'delete',
+            ],
       },
       field: 'operation',
       fixed: 'right',