Explorar el Código

Merge branch 'statistics' into dev

outaozhen hace 4 años
padre
commit
c28b44109d

+ 12 - 0
config/routes.js

@@ -226,6 +226,18 @@ export default [
             name: 'additions',
             component: './Statistic/Customer/CustomerAdditions',
             access: 'authRouteFilter'
+          },
+          {
+            path: '/statistic/customer/source',
+            name: 'source',
+            component: './Statistic/Customer/Source',
+            access: 'authRouteFilter'
+          },
+          {
+            path: '/statistic/customer/nature',
+            name: 'nature',
+            component: './Statistic/Customer/Nature',
+            access: 'authRouteFilter'
           }
         ]
       }

+ 3 - 1
src/locales/zh-CN/menu.js

@@ -49,5 +49,7 @@ export default {
   'menu.statistic.product.trends': '发票收款入账趋势',
   'menu.statistic.product.receivables': '应收款统计',
   'menu.statistic.customer': '客户数据',
-  'menu.statistic.customer.additions': '客户新增'
+  'menu.statistic.customer.additions': '客户新增',
+  'menu.statistic.customer.source': '客户来源',
+  'menu.statistic.customer.nature': '单位性质'
 }

+ 163 - 105
src/pages/Hr/Contacts/index.tsx

@@ -1,91 +1,91 @@
 import BasicTable from '@/components/Table'
+import type { RadioChangeEvent } from 'antd'
 import { PageContainer } from '@ant-design/pro-layout'
-import { Input, List, Popover, Space, Tag } from 'antd'
+import { Input, List, Popover, Radio, Space, Tag } from 'antd'
 import type { ColumnsType } from 'antd/lib/table'
-import React, { useEffect, useMemo, useRef, useState } from 'react'
-import { connect } from '@@/plugin-dva'
+import React, { useState } from 'react'
 import Organization from './components/Organization'
-
-export type ContactsProps = {
-  allStaffList: API.ContractList[]
-}
+import { queryStaffContacts } from '@/services/hr'
+import consts from '@/consts'
+import { isDevMode } from '@/utils/env'
 
 const character = [
-  { id: '1', name: '全部' },
-  { id: '2', name: 'A' },
-  { id: '3', name: 'B' },
-  { id: '4', name: 'C' },
-  { id: '5', name: 'D' },
-  { id: '6', name: 'E' },
-  { id: '7', name: 'F' },
-  { id: '8', name: 'G' },
-  { id: '9', name: 'H' },
-  { id: '10', name: 'I' },
-  { id: '11', name: 'J' },
-  { id: '12', name: 'K' },
-  { id: '13', name: 'L' },
-  { id: '14', name: 'M' },
-  { id: '15', name: 'N' },
-  { id: '16', name: 'O' },
-  { id: '17', name: 'P' },
-  { id: '18', name: 'Q' },
-  { id: '19', name: 'R' },
-  { id: '20', name: 'S' },
-  { id: '21', name: 'T' },
-  { id: '22', name: 'U' },
-  { id: '23', name: 'V' },
-  { id: '24', name: 'W' },
-  { id: '25', name: 'S' },
-  { id: '26', name: 'Y' },
-  { id: '27', name: 'Z' }
+  { value: '0', label: '全部' },
+  { value: 'a', label: 'A' },
+  { value: 'b', label: 'B' },
+  { value: 'c', label: 'C' },
+  { value: 'd', label: 'D' },
+  { value: 'e', label: 'E' },
+  { value: 'f', label: 'F' },
+  { value: 'g', label: 'G' },
+  { value: 'h', label: 'H' },
+  { value: 'i', label: 'I' },
+  { value: 'j', label: 'J' },
+  { value: 'k', label: 'K' },
+  { value: 'l', label: 'L' },
+  { value: 'm', label: 'M' },
+  { value: 'n', label: 'N' },
+  { value: 'o', label: 'O' },
+  { value: 'p', label: 'P' },
+  { value: 'q', label: 'Q' },
+  { value: 'r', label: 'R' },
+  { value: 's', label: 'S' },
+  { value: 't', label: 'T' },
+  { value: 'u', label: 'U' },
+  { value: 'v', label: 'V' },
+  { value: 'w', label: 'W' },
+  { value: 's', label: 'S' },
+  { value: 'y', label: 'Y' },
+  { value: 'z', label: 'Z' }
 ]
 
-const content = (
-  <List
-    dataSource={character}
-    renderItem={item => (
-      <List.Item>
-        <List.Item.Meta title={item.name} />
-      </List.Item>
-    )}
-  />
-)
-
-const Contacts: React.FC<ContactsProps> = props => {
-  const { dispatch, allStaffList = [] } = props
-  const tRef = useRef()
+const Contacts: React.FC<ContactsProps> = () => {
   const [state, setState] = useState({
-    departmentId: '0',
+    data: [],
+    birthdayMonth: [],
+    birthdayDay: [],
+    type: null,
     params: {
+      departmentId: '0',
       search: null
-    }
+    },
+    radioValue: '0'
   })
 
-  const initData = () => {
-    dispatch({
-      type: 'staff/fetchStaffList'
-    })
-  }
-  const dataSource = useMemo(() => {
-    if (state.departmentId === '0') {
-      return allStaffList
-    }
-    return allStaffList.filter(item => item.departmentId === state.departmentId)
-  }, [state.departmentId, allStaffList])
+  const content = (
+    <List
+      itemLayout="horizontal"
+      style={{ maxHeight: '200px', overflow: 'auto' }}
+      dataSource={state.type === 'birthdayMonth' ? state.birthdayMonth : state.birthdayDay}
+      renderItem={item => (
+        <div className="flex items-center py-1">
+          <span
+            className="rounded-1/2 inline-block w-5 h-5 flex items-center"
+            style={{
+              backgroundImage: `url(${
+                (isDevMode() ? 'http://cld2qa.com' : window.location.origin) +
+                (item.avatar ?? consts.DEFAULT_AVATAR)
+              })`,
+              backgroundSize: 'cover'
+            }}
+          />
+          <span className="ml-2">{item.username}</span>
+        </div>
+      )}
+    />
+  )
 
-  useEffect(() => {
-    if (!allStaffList.length) {
-      initData()
-    }
-  }, [])
   const onSelect = id => {
-    setState({ ...state, departmentId: id })
+    setState({ ...state, params: { departmentId: id } })
   }
   const handleSearch = value => {
-    tRef.current?.reloadAndRest()
     setState({ ...state, params: { ...state.params, search: value } })
   }
 
+  const onChangeHandle = ({ target: { value } }: RadioChangeEvent) => {
+    setState({ ...state, radioValue: value, params: { ...state.params, search: value } })
+  }
+
   const columns: ColumnsType<API.ContractItem> = [
     {
       title: '字母',
@@ -141,70 +141,128 @@ const Contacts: React.FC<ContactsProps> = props => {
       <div className="flex flex-row">
         <Organization onSelect={onSelect} />
         <div className="ml-6 shadow-hex-3e2c5a" style={{ width: 'calc(100% - 12rem)' }}>
-          {allStaffList.length ? (
+          {state.params.departmentId && (
             <BasicTable
               rowKey="id"
-              actionRef={tRef}
               columns={columns}
-              dataSource={dataSource}
               params={state.params}
               search={false}
+              request={async params => {
+                const {
+                  code = -1,
+                  data: { list = [], birthdayMonth = [], birthdayDay = [], total = 0 } = {
+                    list: [],
+                    birthdayMonth: [],
+                    birthdayDay: [],
+                    total: 0
+                  }
+                } = await queryStaffContacts({ ...params })
+                setState({ ...state, birthdayMonth: birthdayMonth, birthdayDay: birthdayDay })
+                return {
+                  data: list,
+                  success: code === consts.RET_CODE.SUCCESS,
+                  total
+                }
+              }}
               toolbar={{
                 multipleLine: true,
                 search: (
                   <Input.Search
                     style={{ width: '280px' }}
                     allowClear={true}
-                    placeholder="请输入员工名称或手机进行搜索"
+                    placeholder="请输入员工名称"
                     onSearch={handleSearch}
                   />
                 ),
                 filter: (
                   <div>
                     筛选:
-                    {character?.map(item => {
-                      return (
-                        <span className="inline-flex mb-1" key={item.id}>
-                          <Tag>{item.name}</Tag>
-                        </span>
-                      )
-                    })}
+                    <Radio.Group
+                      defaultValue="0"
+                      options={character}
+                      buttonStyle="solid"
+                      optionType="button"
+                      size="small"
+                      onChange={onChangeHandle}
+                      value={state.radioValue}
+                    />
                   </div>
                 ),
                 actions: [
                   <Space key="id">
-                    <Tag>
-                      本月寿星:
-                      <Popover content={content} title={'本月寿星:'} trigger="click" placement="bottomLeft">
-                        <span>
-                          {character?.map(item => {
-                            return (
-                              <span style={{ marginLeft: -2 }} key={item.id}>
-                                {item.name}
-                              </span>
-                            )
-                          })}
-                        </span>
-                      </Popover>
-                    </Tag>
-                    <Tag>
-                      本日:
-                      <Popover content={content} title={'本日寿星:'} trigger="click" placement="bottomLeft">
-                        AAA
-                      </Popover>
-                    </Tag>
+                    {state.birthdayMonth.length ? (
+                      <Tag>
+                        <Popover
+                          onClick={() => {
+                            setState({ ...state, type: 'birthdayMonth' })
+                          }}
+                          content={content}
+                          title={'本月寿星:'}
+                          trigger="click"
+                          placement="bottomLeft">
+                          <div className="flex items-center cursor-pointer">
+                            本月寿星:
+                            {state.birthdayMonth?.map(item => {
+                              return (
+                                <span
+                                  className="rounded-1/2 inline-block w-4 h-4"
+                                  style={{
+                                    marginLeft: -5,
+                                    backgroundImage: `url(${
+                                      (isDevMode() ? 'http://cld2qa.com' : window.location.origin) +
+                                      (item.avatar ?? consts.DEFAULT_AVATAR)
+                                    })`,
+                                    backgroundSize: 'cover'
+                                  }}
+                                  key={item.id}
+                                />
+                              )
+                            })}
+                          </div>
+                        </Popover>
+                      </Tag>
+                    ) : null}
+                    {state.birthdayDay.length ? (
+                      <Tag>
+                        <Popover
+                          onClick={() => {
+                            setState({ ...state, type: 'birthdayDay' })
+                          }}
+                          content={content}
+                          title={'本日寿星:'}
+                          trigger="click"
+                          placement="bottomLeft">
+                          <div className="flex items-center cursor-pointer">
+                            本日:
+                            {state.birthdayDay?.map(item => {
+                              return (
+                                <span
+                                  className="rounded-1/2 inline-block w-4 h-4"
+                                  style={{
+                                    marginLeft: -5,
+                                    backgroundImage: `url(${
+                                      (isDevMode() ? 'http://cld2qa.com' : window.location.origin) +
+                                      (item.avatar ?? consts.DEFAULT_AVATAR)
+                                    })`,
+                                    backgroundSize: 'cover'
+                                  }}
+                                  key={item.id}
+                                />
+                              )
+                            })}
+                          </div>
+                        </Popover>
+                      </Tag>
+                    ) : null}
                   </Space>
-                ],
-                settings: []
+                ]
               }}
             />
-          ) : null}
+          )}
         </div>
       </div>
     </PageContainer>
   )
 }
 
-export default connect(({ staff }) => ({
-  allStaffList: staff.allStaffList
-}))(Contacts)
+export default Contacts

+ 2 - 2
src/pages/Statistic/Customer/CustomerAdditions/index.tsx

@@ -83,7 +83,7 @@ const CustomerAdditions = () => {
   const handleOnChange = val => {
     // console.log(state.type)
     if (val?.length !== 2) return
-    console.log(val)
+    // console.log(val)
     if (state.params.dateType === 'month') {
       const [startTime, endTime] = val
       setState({
@@ -133,7 +133,7 @@ const CustomerAdditions = () => {
     <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
       <div className="flex flex-col">
         <div className="flex flex-row justify-between mb-4">
-          <div className="card-transition bg-white w-full">
+          <div className="card-transition bg-white w-full h-250px">
             {state.data?.length ? <Column {...columnConfig} /> : null}
           </div>
         </div>

+ 111 - 0
src/pages/Statistic/Customer/Nature/index.tsx

@@ -0,0 +1,111 @@
+import BasicTable from '@/components/Table'
+import consts from '@/consts'
+import { queryNatureList } from '@/services/statistic'
+import { disabledDate } from '@/utils/utils'
+import { PageContainer } from '@ant-design/pro-layout'
+import { Pie } from '@ant-design/charts'
+import { DatePicker, Table } from 'antd'
+import dayjs from 'dayjs'
+import React, { useRef, useState } from 'react'
+import OptionSelect from '../../components/OptionSelect'
+
+const Nature = () => {
+  const [state, setState] = useState({
+    params: {
+      optionType: 'singleStaff',
+      dataIds: [],
+      priceType: 'client',
+      startMonth: [dayjs(new Date()), dayjs(new Date())]
+    },
+    list: []
+  })
+
+  const tRef = useRef(null)
+
+  const handleOptionChange = (type, ids) =>
+    setState({ ...state, params: { ...state.params, optionType: type, dataIds: ids } })
+
+  const columnConfig = {
+    data: state.list.map(item => ({ type: item.name, value: item.count })),
+    appendPadding: 10,
+    angleField: 'value',
+    colorField: 'type',
+    radius: 0.9,
+    legend: {
+      flipPage: false
+    },
+    interactions: [{ type: 'element-active' }],
+    color: ['#40B7E6', '#79CE67', '#FBD743', '#ED9978', '#B8E641', '#E641B8', '#E67041', '#41E6C3']
+  }
+
+  const columns = [
+    {
+      dataIndex: 'name',
+      title: '服务方式',
+      align: 'center'
+    },
+    {
+      dataIndex: 'count',
+      title: '数量',
+      align: 'center',
+      render: text => <div className="text-right">{text}</div>
+    }
+  ]
+  return (
+    <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
+      <div className="flex flex-col">
+        <div className="flex flex-row justify-between mb-4">
+          <div className="card-transition bg-white w-full h-250px">
+            {state.list?.length ? <Pie {...columnConfig} /> : null}
+          </div>
+        </div>
+        <BasicTable
+          search={false}
+          columns={columns}
+          actionRef={tRef}
+          rowkey="ID"
+          params={state.params}
+          pagination={false}
+          bordered
+          toolbar={{
+            title: [<OptionSelect onChange={handleOptionChange} type="Source" key="ID" multiple={false} />],
+            actions: [
+              <DatePicker.RangePicker
+                picker="month"
+                key="DatePicker"
+                allowClear={false}
+                disabledDate={current => disabledDate(current, 'month')}
+                value={state.params.startMonth}
+                onChange={val => setState({ ...state, params: { ...state.params, startMonth: val } })}
+              />
+            ]
+          }}
+          request={async params => {
+            const {
+              code = -1,
+              data: { list, sum }
+            } = await queryNatureList({ ...params })
+            setState({ ...state, sum, list })
+            return {
+              data: list,
+              success: code === consts.RET_CODE.SUCCESS,
+              total: list?.length || 0
+            }
+          }}
+          summary={data =>
+            data?.length ? (
+              <>
+                <Table.Summary.Row>
+                  <Table.Summary.Cell className="text-center">小计</Table.Summary.Cell>
+                  <Table.Summary.Cell className="text-right">{state.sum}</Table.Summary.Cell>
+                </Table.Summary.Row>
+              </>
+            ) : null
+          }
+        />
+      </div>
+    </PageContainer>
+  )
+}
+
+export default Nature

+ 127 - 0
src/pages/Statistic/Customer/Source/index.tsx

@@ -0,0 +1,127 @@
+import BasicTable from '@/components/Table'
+import { PageContainer } from '@ant-design/pro-layout'
+import React, { useRef, useState } from 'react'
+import dayjs from 'dayjs'
+import consts from '@/consts'
+import { disabledDate } from '@/utils/utils'
+import OptionSelect from '../../components/OptionSelect'
+import { DatePicker, Select, Table } from 'antd'
+import { Pie } from '@ant-design/charts'
+import { querySourceList } from '@/services/statistic'
+
+const columnGroupMap = [
+  { label: '客户', value: 'client' },
+  { label: '单位', value: 'company' }
+]
+
+const Source = () => {
+  const [state, setState] = useState({
+    params: {
+      optionType: 'singleStaff',
+      dataIds: [],
+      priceType: 'client',
+      startMonth: [dayjs(new Date()), dayjs(new Date())]
+    },
+    list: []
+  })
+
+  const tRef = useRef(null)
+
+  const handleOptionChange = (type, ids) =>
+    setState({ ...state, params: { ...state.params, optionType: type, dataIds: ids } })
+
+  const columnConfig = {
+    data: state.list.map(item => ({ type: item.sourceName, value: item.count })),
+    appendPadding: 10,
+    angleField: 'value',
+    colorField: 'type',
+    radius: 0.9,
+    legend: {
+      flipPage: false
+    },
+    interactions: [{ type: 'element-active' }],
+    color: ['#40B7E6', '#79CE67', '#FBD743', '#ED9978', '#B8E641', '#E641B8', '#E67041', '#41E6C3']
+  }
+
+  const columns = [
+    {
+      dataIndex: 'sourceName',
+      title: '服务方式',
+      align: 'center'
+    },
+    {
+      dataIndex: 'count',
+      title: '数量',
+      align: 'center',
+      render: text => <div className="text-right">{text}</div>
+    }
+  ]
+  return (
+    <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
+      <div className="flex flex-col">
+        <div className="flex flex-row justify-between mb-4">
+          <div className="card-transition bg-white w-full h-250px">
+            {/* 扇形图 */}
+            {state.list?.length ? <Pie {...columnConfig} /> : null}
+          </div>
+        </div>
+        <BasicTable
+          search={false}
+          columns={columns}
+          actionRef={tRef}
+          rowkey="sourceId"
+          params={state.params}
+          pagination={false}
+          bordered
+          toolbar={{
+            title: [
+              <OptionSelect onChange={handleOptionChange} type="Source" key="sourceId" multiple={false} />,
+              <div className="ml-2" key="id">
+                <Select
+                  defaultValue="client"
+                  options={columnGroupMap}
+                  placeholder="请选择类型"
+                  onChange={e => setState({ ...state, params: { ...state.params, priceType: e } })}
+                />
+              </div>
+            ],
+            actions: [
+              <DatePicker.RangePicker
+                picker="month"
+                key="DatePicker"
+                allowClear={false}
+                disabledDate={current => disabledDate(current, 'month')}
+                value={state.params.startMonth}
+                onChange={val => setState({ ...state, params: { ...state.params, startMonth: val } })}
+              />
+            ]
+          }}
+          request={async params => {
+            const {
+              code = -1,
+              data: { list, sum }
+            } = await querySourceList({ ...params })
+            setState({ ...state, sum, list })
+            return {
+              data: list,
+              success: code === consts.RET_CODE.SUCCESS,
+              total: list?.length || 0
+            }
+          }}
+          summary={data =>
+            data?.length ? (
+              <>
+                <Table.Summary.Row>
+                  <Table.Summary.Cell className="text-center">小计</Table.Summary.Cell>
+                  <Table.Summary.Cell className="text-right">{state.sum}</Table.Summary.Cell>
+                </Table.Summary.Row>
+              </>
+            ) : null
+          }
+        />
+      </div>
+    </PageContainer>
+  )
+}
+
+export default Source

+ 1 - 1
src/pages/Statistic/Product/InvoiceAggregate/index.tsx

@@ -87,7 +87,7 @@ const InvoiceAggregate = () => {
     <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
       <div className="flex flex-col">
         <div className="flex flex-row justify-between mb-4">
-          <div className="card-transition bg-white w-full">
+          <div className="card-transition bg-white w-full h-250px">
             {/* {pieData.pieLeft ? <Pie {...leftConfig} /> : null} */}
             {state.list?.length ? <Column {...columnConfig} /> : null}
           </div>

+ 1 - 1
src/pages/Statistic/Product/InvoiceTrends/index.tsx

@@ -183,7 +183,7 @@ const InvoiceTrends = () => {
     <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
       <div className="flex flex-col">
         <div className="flex flex-row justify-between mb-4">
-          <div className="card-transition bg-white w-full">
+          <div className="card-transition bg-white w-full h-250px">
             {state.chart?.length ? <DualAxes {...dualAxesOptions} /> : null}
             {/* {pieData.pieLeft ? <Pie {...leftConfig} /> : null} */}
             {/* {state.list?.length ? <Column {...columnConfig} /> : null} */}

+ 2 - 2
src/pages/Statistic/Product/SoftwareUsage/index.tsx

@@ -123,10 +123,10 @@ const Usage = () => {
     <PageContainer title={false} breadcrumb={false} waterMarkProps={{ offsetTop: 111 }}>
       <div className="flex flex-col">
         <div className="flex flex-row justify-between mb-4">
-          <div className="card-transition  w-1/2 bg-white mr-2">
+          <div className="card-transition w-1/2 h-250px bg-white mr-2">
             {pieData.pieLeft ? <Pie {...leftConfig} /> : null}
           </div>
-          <div className="card-transition w-1/2 bg-white ml-2">
+          <div className="card-transition w-1/2 h-250px bg-white ml-2">
             {pieData.pieRight ? <Pie {...rightConfig} /> : null}
           </div>
         </div>

+ 6 - 3
src/pages/Workbench/Dashboard/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from 'react'
+import React, { useState, useEffect, useRef, CSSProperties } from 'react'
 import ProTable from '@ant-design/pro-table'
 import { Row, Col, Select, Card, Skeleton, TreeSelect } from 'antd'
 import { AddressBook, City, Comment } from '@icon-park/react'
@@ -60,9 +60,10 @@ const Dashboard = ({ dispatch, departments = [] }) => {
     dispatchModal('M_REMINDER_LIST', { dataType, reminderCyclical })
   }
   // 展示卡片详情
-  const handleRatioCard = dataType => {
+  const handleRatioCard = (dataType: CardTypeMap, bodyStyle: CSSProperties) => {
     dispatchModal('M_RATIO_PANELS', {
       dataType,
+      bodyStyle,
       staffIds: state.params.staffIds,
       retioCyclical: state.params.cyclical,
       dataPermission: state.params.dataPermission
@@ -519,7 +520,9 @@ const Dashboard = ({ dispatch, departments = [] }) => {
                 {state.loading ? (
                   <Skeleton active avatar />
                 ) : (
-                  <Row onClick={() => handleRatioCard(CardTypeMap.SERVICE)} className="cursor-pointer">
+                  <Row
+                    onClick={() => handleRatioCard(CardTypeMap.SERVICE, { paddingTop: 'unset' })}
+                    className="cursor-pointer">
                     <Col span={16}>
                       <div className="flex items-center">
                         <div className="w-12 h-12 rounded-1/2 bg-hex-0c7cd5 flex items-center justify-center">

+ 8 - 2
src/services/hr.ts

@@ -29,6 +29,12 @@ export async function queryDepartmentStaffList() {
 }
 
 /** 获取通讯录人员列表 */
-export async function queryStaffContacts() {
-  return request('/staff/contacts')
+export async function queryStaffContacts(payload) {
+  const data = await request('/staff/listV2', {
+    params: { ...payload }
+  })
+  return data
 }
+// export async function queryStaffContacts() {
+//   return request('/staff/listV2')
+// }

+ 16 - 0
src/services/statistic.ts

@@ -44,3 +44,19 @@ export async function queryInvoiceReceivablesList(params) {
     data: params
   })
 }
+
+/** 来源统计 */
+export async function querySourceList(params) {
+  return request('/statistic/source/statistics', {
+    method: 'POST',
+    data: params
+  })
+}
+
+/** 单位性质统计 */
+export async function queryNatureList(params) {
+  return request('/statistic/unit/nature', {
+    method: 'POST',
+    data: params
+  })
+}