Jelajahi Sumber

feat: 出库流水列表

xyh 3 tahun lalu
induk
melakukan
4f90670af3

+ 1 - 1
cypress/e2e/storage.cy.ts

@@ -102,7 +102,7 @@ describe('库位管理', function() {
       cy.getTestId('field_storageLocationCode').should('have.value', '0001');
       cy.getTestId('field_storageLocationName').should('have.value', '1号库位');
       validateSelect('select_storageLocationType', '原材料库位');
-      validateSelect('select_storageIsNotDisable', '启用');
+      validateSelect('select_storageIsNotDisable', '');
     });
 
     validateDelete(TABLE_NAME, LABEL);

+ 28 - 1
packages/app/src/apis/stream.ts

@@ -1,4 +1,10 @@
-import {BaseListResult, GetWarehousingFlowingListParams, WarehousingListData} from '@models';
+import {
+  BaseListResult,
+  GetRawOutListParams,
+  GetWarehousingFlowingListParams,
+  RawMaterialOutStreamListData,
+  WarehousingListData,
+} from '@models';
 import {request} from './request';
 
 /** 获取入库流水 */
@@ -23,3 +29,24 @@ export function exportWarehousing(
     skipError: true,
   });
 }
+
+/** 出库流水 */
+export function getRawOutList(
+  data: GetRawOutListParams,
+): BaseListResult<RawMaterialOutStreamListData> {
+  return request({
+    method: 'GET',
+    url: '/askGoods/getRemoval',
+    data,
+  });
+}
+
+/** 导出出库流水 */
+export function exportRawOut(data: GetRawOutListParams): any {
+  return request({
+    method: 'GET',
+    url: '/askGoods/export',
+    skipError: true,
+    data,
+  });
+}

+ 13 - 0
packages/app/src/hooks/useOptions/index.test.tsx

@@ -0,0 +1,13 @@
+import {useDictionaryOptions, useRoleOptions, useStorageOptions} from '.';
+
+describe('useOptions', function() {
+  it('声明', function() {
+    expect(useStorageOptions).toBeDefined();
+    expect(useRoleOptions).toBeDefined();
+    expect(useDictionaryOptions).toBeDefined();
+
+    expect(useStorageOptions).toBeInstanceOf(Function);
+    expect(useRoleOptions).toBeInstanceOf(Function);
+    expect(useDictionaryOptions).toBeInstanceOf(Function);
+  });
+});

+ 76 - 0
packages/app/src/hooks/usePutData/index.test.tsx

@@ -0,0 +1,76 @@
+import {BaseResult} from '@models';
+import {usePutData} from '.';
+import {render, waitFor} from '@testing-library/react';
+import {FC} from 'react';
+import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
+
+const fn = jest.fn();
+
+function mockRequest(): BaseResult {
+  fn();
+  return new Promise(function(res) {
+    setTimeout(() => res({msg: '200', data: 123}), 500);
+  });
+}
+
+describe('usePutData', function() {
+  it('声明', function() {
+    expect(usePutData).toBeDefined();
+    expect(usePutData).toBeInstanceOf(Function);
+  });
+
+  it('返回值', async function() {
+    jest.useFakeTimers();
+
+    const App: FC = function() {
+      const [isLoading, {addMutate, editMutate}] = usePutData({
+        addFn: mockRequest,
+        editFn: mockRequest,
+        onClose: fn,
+        onFetch: fn,
+      });
+
+      return (
+        <>
+          <p data-testid='state'>{isLoading.toString()}</p>
+          <button data-testid='add_btn' onClick={addMutate} />
+          <button data-testid='edit_btn' onClick={editMutate} />
+        </>
+      );
+    };
+
+    const client = new QueryClient();
+    const {getByTestId} = render(
+      <QueryClientProvider client={client}>
+        <App />
+      </QueryClientProvider>,
+    );
+
+    expect(getByTestId('state').innerHTML).toBe('false');
+
+    getByTestId('add_btn').click();
+
+    await waitFor(function() {
+      expect(getByTestId('state').innerHTML).toBe('true');
+    });
+
+    jest.advanceTimersByTime(500);
+    await waitFor(function() {
+      expect(getByTestId('state').innerHTML).toBe('false');
+    });
+
+    expect(fn).toBeCalledTimes(3);
+
+    getByTestId('edit_btn').click();
+    await waitFor(function() {
+      expect(getByTestId('state').innerHTML).toBe('true');
+    });
+
+    jest.advanceTimersByTime(500);
+    await waitFor(function() {
+      expect(getByTestId('state').innerHTML).toBe('false');
+    });
+
+    expect(fn).toBeCalledTimes(6);
+  });
+});

+ 27 - 0
packages/app/src/hooks/useRangeDate/index.test.ts

@@ -0,0 +1,27 @@
+import {renderHook} from '@testing-library/react';
+import {useRangeDate} from '.';
+import dayjs from 'dayjs';
+import {act} from 'react-dom/test-utils';
+
+describe('useRangeDate', function() {
+  it('声明', function() {
+    expect(useRangeDate).toBeDefined();
+    expect(useRangeDate).toBeInstanceOf(Function);
+  });
+
+  it('返回值', function() {
+    const {result} = renderHook(function() {
+      return useRangeDate(dayjs(), dayjs());
+    });
+
+    expect(result.current[0].start).toBe('2023-03-09');
+    expect(result.current[0].end).toBe('2023-03-09');
+
+    act(function() {
+      result.current[1]([dayjs().add(1, 'day'), dayjs().add(2, 'day')]);
+    });
+
+    expect(result.current[0].start).toBe('2023-03-10');
+    expect(result.current[0].end).toBe('2023-03-11');
+  });
+});

+ 2 - 0
packages/app/src/models/request/stream.ts

@@ -9,3 +9,5 @@ export type GetWarehousingFlowingListParams = {
   /** 物料编号 */
   wllbCode: string;
 } & ListParams;
+
+export type GetRawOutListParams = GetWarehousingFlowingListParams & {type: string};

+ 7 - 0
packages/app/src/models/response/stream.ts

@@ -102,8 +102,15 @@ export type RawMaterialOutStreamListData = {
   * 用户编号
   */
   userId: string;
+  /** 用户名称 */
+  useName: string;
   /**
   * 物料编号
   */
   wllbCode: string;
+  /** 供应商名称 */
+  supplierName: string;
+  /** 部门名称 */
+  departmentName: string;
+
 };

+ 8 - 1
packages/app/src/pages/main/finish/index.tsx

@@ -3,6 +3,7 @@ import css from '../index.module.css';
 import {FC} from 'react';
 import {ResponsiveContainer, XAxis, YAxis, Tooltip, Legend, AreaChart, Area} from 'recharts';
 import classNames from 'classnames';
+import {useFullscreen} from '../hooks';
 
 const data = [
   {
@@ -48,8 +49,14 @@ const data = [
 ];
 
 const FinishChart: FC = function() {
+  const ref = useFullscreen();
+
   return (
-    <Card title='产成品出入库统计' className={classNames(css.card, css.flexCard)}>
+    <Card
+      ref={ref}
+      title='产成品出入库统计'
+      className={classNames(css.card, css.flexCard)}
+    >
       <ResponsiveContainer>
         <AreaChart data={data}>
           <XAxis dataKey='name' />

+ 19 - 0
packages/app/src/pages/main/hooks.ts

@@ -0,0 +1,19 @@
+import {useEffect, useRef} from 'react';
+
+export function useFullscreen() {
+  const ref = useRef<HTMLDivElement>(null);
+
+  useEffect(function() {
+    function onTitleClick() {
+      ref.current?.requestFullscreen();
+    }
+
+    ref.current?.querySelector('.ant-card-head')
+      ?.addEventListener('click', onTitleClick);
+
+    () => ref.current?.querySelector('.ant-card-head')
+      ?.removeEventListener('click', onTitleClick);
+  });
+
+  return ref;
+}

+ 4 - 0
packages/app/src/pages/main/index.module.css

@@ -6,6 +6,10 @@
     flex: 1;
     overflow: hidden;
   }
+
+  & :global(.ant-card-head) {
+    cursor: pointer;
+  }
 }
 
 .card {

+ 8 - 1
packages/app/src/pages/main/raw/index.tsx

@@ -3,6 +3,7 @@ import {FC} from 'react';
 import {ResponsiveContainer, Bar, XAxis, YAxis, BarChart, Tooltip, Legend} from 'recharts';
 import css from '../index.module.css';
 import classNames from 'classnames';
+import {useFullscreen} from '../hooks';
 
 const data = [
   {
@@ -63,8 +64,14 @@ const data = [
 ];
 
 const RawChart: FC = function() {
+  const ref = useFullscreen();
+
   return (
-    <Card title='原材料出入库统计' className={classNames(css.card, css.flexCard)}>
+    <Card
+      title='原材料出入库统计'
+      className={classNames(css.card, css.flexCard)}
+      ref={ref}
+    >
       <ResponsiveContainer>
         <BarChart data={data}>
           <XAxis dataKey='name' />

+ 2 - 1
packages/app/src/pages/raw-out-stream/context.ts

@@ -9,12 +9,13 @@ type State = {
   startTime: string;
   endTime: string;
   wllbCode: string;
+  type: string;
 };
 
 type Action = {type: 'SEARCH', payload: State};
 
 function initState(): State {
-  return {startTime: '', endTime: '', wllbCode: ''};
+  return {startTime: '', endTime: '', wllbCode: '', type: ''};
 }
 
 function reducer(state: State, action: Action): State {

+ 9 - 4
packages/app/src/pages/raw-out-stream/filter/hooks.ts

@@ -1,14 +1,19 @@
 import {useContextSection, useExportFile, usePage, useTableSearch} from '@hooks';
 import {context, pageContext, searchContext} from '../context';
 import {useContextSelector} from 'use-context-selector';
-import {exportWarehousing} from '@apis';
+import {exportRawOut} from '@apis';
 
-export function useSearch(code: string, startTime: string, endTime: string) {
+export function useSearch(options: {
+  wllbCode: string,
+  startTime: string,
+  endTime: string,
+  type: string
+}) {
   const [isSearching] = useTableSearch(searchContext);
   const dispatch = useContextSection(context, state => state[1]);
 
   function onSearch() {
-    dispatch({type: 'SEARCH', payload: {startTime, endTime, wllbCode: code}});
+    dispatch({type: 'SEARCH', payload: options});
   }
 
   return [isSearching, onSearch] as const;
@@ -16,7 +21,7 @@ export function useSearch(code: string, startTime: string, endTime: string) {
 
 export function useExport() {
   const params = useContextSelector(context, state => state[0]);
-  const [isExporting, mutate] = useExportFile(exportWarehousing);
+  const [isExporting, mutate] = useExportFile(exportRawOut);
   const [{page, pageSize}] = usePage(pageContext);
 
   function onExport() {

+ 12 - 7
packages/app/src/pages/raw-out-stream/filter/index.tsx

@@ -1,27 +1,32 @@
 import {FilterButtonGroup, FilterDatePicker, FilterField} from '@components';
-import {useRangeDate} from '@hooks';
+import {useFilterField, useRangeDate} from '@hooks';
 import {Card, Row} from 'antd';
-import {FC, useState} from 'react';
+import {FC} from 'react';
 import {useExport, useSearch} from './hooks';
 
 const Filter: FC = function() {
   const [{dates, start, end}, onDatesChange] = useRangeDate();
-  const [code, onCodeChange] = useState('');
-  const [isSearching, onSearch] = useSearch(code, start, end);
+  const [{code, type}, onChange] = useFilterField({code: '', type: ''});
+  const [isSearching, onSearch] = useSearch({
+    startTime: start,
+    endTime: end,
+    wllbCode: code,
+    type,
+  });
   const [isExporting, onExport] = useExport();
 
   return (
     <Card>
       <Row>
-        <FilterField name='rawMaterialCode' label='物料编号' value={code} onChange={onCodeChange} />
+        <FilterField name='rawMaterialCode' label='物料编号' value={code} onChange={onChange('code')} />
+        <FilterField name='rawMaterialType' label='出库类型' value={code} onChange={onChange('type')} />
         <FilterDatePicker
           name='rawMaterialDates'
-          label='库时间'
+          label='库时间'
           value={dates}
           onChange={onDatesChange}
         />
         <FilterButtonGroup
-          offset={6}
           onSearch={onSearch}
           isSearching={isSearching}
           onExport={onExport}

+ 2 - 0
packages/app/src/pages/raw-out-stream/index.tsx

@@ -3,6 +3,7 @@ import {FC} from 'react';
 import {context, pageContext, searchContext, useContextReducer} from './context';
 import {PageProvider, SearchProvider} from '@components';
 import Filter from './filter';
+import TableList from './table';
 
 const RawMaterialInStreamProvider: ChildrenFC = function({children}) {
   const {Provider} = context;
@@ -18,6 +19,7 @@ const RawMaterialInStream: FC = function() {
         <SearchProvider context={searchContext}>
           <section className='content-main'>
             <Filter />
+            <TableList />
           </section>
         </SearchProvider>
       </PageProvider>

+ 14 - 0
packages/app/src/pages/raw-out-stream/table/hooks.ts

@@ -0,0 +1,14 @@
+import {useContextSection, useQueryTableList} from '@hooks';
+import {context, pageContext, searchContext} from '../context';
+import {getRawOutList} from '@apis';
+
+export function useList() {
+  const params = useContextSection(context, state => state[0]);
+
+  return useQueryTableList({
+    queryFn: getRawOutList,
+    params,
+    pageContext,
+    searchContext,
+  });
+}

+ 79 - 0
packages/app/src/pages/raw-out-stream/table/index.tsx

@@ -0,0 +1,79 @@
+import {FC} from 'react';
+import {useList} from './hooks';
+import {RawMaterialOutStreamListData} from '@models';
+import {ColumnsType} from 'antd/es/table';
+import {Table} from '@components';
+import {pageContext, searchContext} from '../context';
+import {Card} from 'antd';
+
+const columns: ColumnsType<RawMaterialOutStreamListData> = [
+  {
+    title: '物料编号',
+    dataIndex: 'wllbCode',
+    key: 'wllbCode',
+  },
+  {
+    title: '物料名称',
+    dataIndex: 'materialName',
+    key: 'materialName',
+  },
+  {
+    title: '供应商名称',
+    dataIndex: 'supplierName',
+    key: 'supplierName',
+  },
+  {
+    title: '连翻号',
+    dataIndex: 'serial',
+    key: 'serial',
+  },
+  {
+    title: '类型',
+    dataIndex: 'type',
+    key: 'type',
+  },
+  {
+    title: '出库用户',
+    dataIndex: 'userName',
+    key: 'userName',
+  },
+  {
+    title: '领用部门',
+    dataIndex: 'department',
+    key: 'department',
+  },
+  {
+    title: '库位名称',
+    dataIndex: 'storageLocationName',
+    key: 'storageLocationName',
+  },
+  {
+    title: '出库日期',
+    dataIndex: 'scrq',
+    key: 'scrq',
+  },
+  {
+    title: '出库数量',
+    dataIndex: 'num',
+    key: 'num',
+  },
+];
+
+const TableList: FC = function() {
+  const [{data, count}] = useList();
+
+  return (
+    <Card className='table-wrapper'>
+      <Table
+        data={data}
+        count={count}
+        pageContext={pageContext}
+        searchContext={searchContext}
+        columns={columns}
+        data-testid='raw_out_stream_table'
+      />
+    </Card>
+  );
+};
+
+export default TableList;

+ 53 - 0
pnpm-lock.yaml

@@ -121,6 +121,7 @@ importers:
       '@swc/core': ^1.3.23
       '@swc/jest': ^0.2.24
       '@testing-library/dom': ^8.19.0
+      '@types/speed-measure-webpack-plugin': ^1.3.4
       browserslist: ^4.21.4
       camelcase: '6'
       chalk: '4'
@@ -146,6 +147,7 @@ importers:
       prompts: ^2.4.2
       react-app-polyfill: ^3.0.0
       react-refresh: ^0.14.0
+      speed-measure-webpack-plugin: ^1.5.0
       style-loader: ^3.3.1
       swc-loader: ^0.2.3
       swc-plugin-react-remove-properties: ^0.1.1
@@ -161,6 +163,7 @@ importers:
       '@swc/core': 1.3.23
       '@swc/jest': 0.2.24_@swc+core@1.3.23
       '@testing-library/dom': 8.19.0
+      '@types/speed-measure-webpack-plugin': 1.3.4
       browserslist: 4.21.4
       camelcase: 6.3.0
       chalk: 4.1.2
@@ -186,6 +189,7 @@ importers:
       prompts: 2.4.2
       react-app-polyfill: 3.0.0
       react-refresh: 0.14.0
+      speed-measure-webpack-plugin: 1.5.0_webpack@5.75.0
       style-loader: 3.3.1_webpack@5.75.0
       swc-loader: 0.2.3_zqhpc3y2oecp2qxc3hqu7q4zbq
       swc-plugin-react-remove-properties: 0.1.1
@@ -2978,10 +2982,24 @@ packages:
       '@types/node': 18.11.18
     dev: true
 
+  /@types/source-list-map/0.1.2:
+    resolution: {integrity: sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==}
+    dev: true
+
+  /@types/speed-measure-webpack-plugin/1.3.4:
+    resolution: {integrity: sha512-bTV+ZctiMPqufZXEnfkNL4DgXzgkq0AnN2hnwqfEZn5ZqqxbGi55Rtp3vrnr8U5jgFJoYFONQCCkGPWsLOT2Sg==}
+    dependencies:
+      '@types/webpack': 4.41.33
+    dev: true
+
   /@types/stack-utils/2.0.1:
     resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
     dev: true
 
+  /@types/tapable/1.0.8:
+    resolution: {integrity: sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==}
+    dev: true
+
   /@types/testing-library__jest-dom/5.14.5:
     resolution: {integrity: sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==}
     dependencies:
@@ -2992,6 +3010,31 @@ packages:
     resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==}
     dev: true
 
+  /@types/uglify-js/3.17.1:
+    resolution: {integrity: sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==}
+    dependencies:
+      source-map: 0.6.1
+    dev: true
+
+  /@types/webpack-sources/3.2.0:
+    resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==}
+    dependencies:
+      '@types/node': 18.11.18
+      '@types/source-list-map': 0.1.2
+      source-map: 0.7.4
+    dev: true
+
+  /@types/webpack/4.41.33:
+    resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==}
+    dependencies:
+      '@types/node': 18.11.18
+      '@types/tapable': 1.0.8
+      '@types/uglify-js': 3.17.1
+      '@types/webpack-sources': 3.2.0
+      anymatch: 3.1.3
+      source-map: 0.6.1
+    dev: true
+
   /@types/ws/8.5.3:
     resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
     dependencies:
@@ -10508,6 +10551,16 @@ packages:
       - supports-color
     dev: true
 
+  /speed-measure-webpack-plugin/1.5.0_webpack@5.75.0:
+    resolution: {integrity: sha512-Re0wX5CtM6gW7bZA64ONOfEPEhwbiSF/vz6e2GvadjuaPrQcHTQdRGsD8+BE7iUOysXH8tIenkPCQBEcspXsNg==}
+    engines: {node: '>=6.0.0'}
+    peerDependencies:
+      webpack: ^1 || ^2 || ^3 || ^4 || ^5
+    dependencies:
+      chalk: 4.1.2
+      webpack: 5.75.0_id63hl4ti3a2o54kosxu7nkk2i
+    dev: true
+
   /sprintf-js/1.0.3:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
     dev: true