Commit 0ce641f8a794e6c6dbd0320fc56f053fa4de1877

Authored by 张志伟
2 parents 014fcdfa 2252127c

Merge branch 'Shinner-sale-task' into 'master'

零售任务分配设置



See merge request !255
config/routers/order3.ts
... ... @@ -50,14 +50,14 @@ export default [
50 50 component: "./order3/SpcManagement",
51 51 },
52 52 {
53   - //零售任务手动调整
54   - path: "/order3/retailTask",
55   - component: "./order3/RetailTask",
  53 + //零售任务分配
  54 + path: "/order3/saleTask",
  55 + component: "./order3/SaleTask",
56 56 },
57 57 {
58   - //零售任务自动分配
59   - path: "/order3/RetailManualAdjust",
60   - component: "./order3/RetailManualAdjust",
  58 + //零售任务编辑
  59 + path: "/order3/saleTask/edit",
  60 + component: "./order3/SaleTask/subpages/TaskEdit",
61 61 },
62 62 {
63 63 //攻坚车型任务手动分配
... ...
src/pages/cas/GrossProfit/subpages/Add/components/AdviserTable.tsx
... ... @@ -29,7 +29,6 @@ const AdviserTable = ({ params, showBizTypeModal }: AdviserTableProps) => {
29 29 pagination={paginationConfig}
30 30 rowKey="id"
31 31 loading={loading}
32   - childrenColumnName="other"
33 32 >
34 33 <Column title="人员" dataIndex="name" />
35 34 <Column
... ...
src/pages/cas/GrossProfit/subpages/Add/components/BizTypeTable.tsx
... ... @@ -29,7 +29,6 @@ const BizTypeTable = ({ params, showAdviserModal }: BizTypeTableProps) =&gt; {
29 29 pagination={paginationConfig}
30 30 rowKey="id"
31 31 loading={loading}
32   - childrenColumnName="other"
33 32 >
34 33 <Column title="业务类型" dataIndex="name" />
35 34 <Column title="毛利目标(万元)" dataIndex="doubleGrossProfitTarget" />
... ...
src/pages/cas/GrossProfit/subpages/Add/components/GPConfirm.tsx
... ... @@ -192,7 +192,6 @@ export default function GPConfirm({ settingId }: GPConfirmProps) {
192 192 pagination={paginationConfig}
193 193 rowKey="id"
194 194 loading={loading}
195   - childrenColumnName="other"
196 195 >
197 196 <Column title={QueryTypeObj[queryType]} dataIndex="name" />
198 197 <Column title="毛利目标(万元)" dataIndex="doubleGrossProfitTarget" />
... ...
src/pages/cas/GrossProfit/subpages/Add/components/GPSetting.tsx
... ... @@ -209,7 +209,6 @@ const GPSetting = ({ settingId, filterMonth, setCurrStep }: GPSettingProps) =&gt; {
209 209 pagination={false}
210 210 rowKey="id"
211 211 loading={loading}
212   - childrenColumnName="other"
213 212 >
214 213 <Column title="门店" dataIndex="shopName" />
215 214 <Column title="净利目标(万元)" dataIndex="doubleNetProfitTarget" />
... ...
src/pages/cas/GrossProfit/subpages/Add/components/ShopTable.tsx
... ... @@ -28,7 +28,6 @@ const ShopTable = ({ params, showAdviserModal }: ShopTableProps) =&gt; {
28 28 pagination={paginationConfig}
29 29 rowKey="id"
30 30 loading={loading}
31   - childrenColumnName="other"
32 31 >
33 32 <Column title="门店" dataIndex="name" />
34 33 <Column title="毛利目标(万元)" dataIndex="doubleGrossProfitTarget" />
... ...
src/pages/order3/SaleTask/api.ts 0 → 100644
  1 +import { http } from "@/typing/http";
  2 +import request from "@/utils/request";
  3 +import { FVM_HOST, ORDER3_HOST } from "@/utils/host";
  4 +
  5 +type PromiseResp<T> = http.PromiseResp<T>;
  6 +
  7 +export interface GetSaleTaskApiReq {
  8 + taskDate?: number;
  9 + shopName?: string;
  10 +}
  11 +
  12 +export interface GetSaleTaskApiRes {
  13 + id: number;
  14 + totalTaskCount: number;
  15 + shopTaskList: ShopTaskItem[];
  16 + year: number;
  17 + month: number;
  18 + canModified: boolean; // 是否可以编辑
  19 + revoke: boolean; // 是否可以撤销
  20 + newEnergyTaskCount: number;
  21 + fuelVehicleTaskCount: number;
  22 + clueDealTaskCount: number;
  23 + approvalNumber: string;
  24 + tackCarTaskCount: number;
  25 + testDriveTaskCount: number;
  26 + seriesTaskCount: number;
  27 + vehicleGrossProfitTask: number;
  28 +}
  29 +
  30 +export interface SeriesTaskItem {
  31 + brandId: number;
  32 + brandName: string;
  33 + seriesId: number;
  34 + seriesName: string;
  35 + newEnergy: boolean;
  36 + taskCount: number;
  37 + id?: number;
  38 +}
  39 +
  40 +export interface StaffTaskItem {
  41 + id?: number;
  42 + staffId: number;
  43 + staffName: string;
  44 + taskCount: number;
  45 + clueDealTaskCount: number;
  46 + regularMonth: number;
  47 + addedValueTask: number;
  48 + seriesTaskList: SeriesTaskItem[];
  49 + newEnergyTaskCount: number;
  50 + fuelVehicleTaskCount: number;
  51 + tackCarTaskCount: number;
  52 + testDriveTaskCount: number;
  53 + seriesTaskCount: number;
  54 + vehicleGrossProfitTask: number;
  55 +}
  56 +
  57 +export interface ShopTaskItem {
  58 + id?: number;
  59 + shopId: number;
  60 + shopName: string;
  61 + taskCount: number;
  62 + clueDealTaskCount: number;
  63 + clueDealTaskRate: number;
  64 + staffTaskList: StaffTaskItem[];
  65 + addedValueTask: number;
  66 + seriesTaskList: SeriesTaskItem[];
  67 + newEnergyTaskCount: number;
  68 + fuelVehicleTaskCount: number;
  69 + tackCarTaskCount: number;
  70 + testDriveTaskCount: number;
  71 + seriesTaskCount: number;
  72 + vehicleGrossProfitTask: number;
  73 + taskId?: number;
  74 +}
  75 +
  76 +/** 月度零售任务列表 */
  77 +export function getSaleTaskApi(
  78 + params: GetSaleTaskApiReq
  79 +): PromiseResp<GetSaleTaskApiRes> {
  80 + return request.get(`${ORDER3_HOST}/erp/sales/task/detail`, { params });
  81 +}
  82 +
  83 +export interface GetShopSaleTaskReq {
  84 + shopId: number;
  85 + taskDate: number;
  86 +}
  87 +
  88 +/** 门店零售任务详情 */
  89 +export function getShopSaleTask(
  90 + params: GetShopSaleTaskReq
  91 +): PromiseResp<ShopTaskItem> {
  92 + return request.get(`${ORDER3_HOST}/erp/sales/task/detail/one`, { params });
  93 +}
  94 +
  95 +/** 保存门店零售任务 */
  96 +export function saveShopSaleTask(params: ShopTaskItem): PromiseResp<boolean> {
  97 + return request.post(`${ORDER3_HOST}/erp/sales/task/manual/assign`, params);
  98 +}
  99 +
  100 +/** 撤销审批零售任务 */
  101 +export function cancelSaleTask(params: { id: number }): PromiseResp<boolean> {
  102 + return request.post(`${ORDER3_HOST}/erp/sales/task/cancel/approve`, params, {
  103 + contentType: "form-urlencoded",
  104 + });
  105 +}
  106 +
  107 +/** 提交审批零售任务 */
  108 +export function submitSaleTask(params: { id: number }): PromiseResp<boolean> {
  109 + return request.post(`${ORDER3_HOST}/erp/sales/task/submit`, params);
  110 +}
  111 +
  112 +export interface BrandItem {
  113 + id: number;
  114 + initial: string;
  115 + name: string;
  116 + thumbnail: string;
  117 +}
  118 +
  119 +/** 门店下面品牌列表 */
  120 +export function getBrands(params: {
  121 + shopId: number;
  122 +}): PromiseResp<BrandItem[]> {
  123 + return request.get(`${FVM_HOST}/erp/putaway/vehicle/support/brand`, {
  124 + params,
  125 + });
  126 +}
  127 +
  128 +export interface SeriesItem {
  129 + id: number;
  130 + name: string;
  131 + newEnergy: boolean;
  132 + thumbnail: string;
  133 +}
  134 +
  135 +/** 门店某品牌下车系列表 */
  136 +export function getSeries(params: {
  137 + shopId: number;
  138 + brandId: number;
  139 +}): PromiseResp<SeriesItem[]> {
  140 + return request.get(`${FVM_HOST}/erp/putaway/vehicle/support/series`, {
  141 + params,
  142 + });
  143 +}
  144 +
  145 +export interface PreviewTaskReq {
  146 + shopId?: number;
  147 + firstManageId?: number;
  148 + secondManageId?: number;
  149 + thirdManageId?: number;
  150 + taskId?: number;
  151 + orderTaskApprovalType: number;
  152 + id: number;
  153 + token?: string;
  154 +}
  155 +
  156 +export interface TaskListItem {
  157 + id: number;
  158 + dataId: number;
  159 + dataName: string;
  160 + beforeTaskCount: number;
  161 + afterTaskCount: number;
  162 + beforeClueDealTaskCount: number;
  163 + afterClueDealTaskCount: number;
  164 + beforeNewEnergyTaskCount: number;
  165 + afterNewEnergyTaskCount: number;
  166 + beforeFuelVehicleTaskCount: number;
  167 + afterFuelVehicleTaskCount: number;
  168 + beforeTackCarTaskCount: number;
  169 + afterTackCarTaskCount: number;
  170 + beforeTestDriveTaskCount: number;
  171 + afterTestDriveTaskCount: number;
  172 + taskCountModified: boolean;
  173 + clueDealTaskCountModified: boolean;
  174 + newEnergyTaskCountModified: boolean;
  175 + fuelVehicleTaskCountModified: boolean;
  176 + tackCarTaskCountModified: boolean;
  177 + testDriveTaskCountModified: boolean;
  178 + seriesTaskCount: number;
  179 + newEnergy: boolean;
  180 +}
  181 +
  182 +export interface PreviewTaskRes {
  183 + year: number;
  184 + month: number;
  185 + remark: string;
  186 + attachmentList: string[];
  187 + taskList: TaskListItem[];
  188 +}
  189 +
  190 +/** 预览任务 */
  191 +export function previewTask(params: PreviewTaskReq): PromiseResp<PreviewTaskRes> {
  192 + return request.get(`${ORDER3_HOST}/erp/sales/task/approve/info`, {
  193 + params,
  194 + });
  195 +}
... ...
src/pages/order3/SaleTask/components/AdviserTaskPreview.tsx 0 → 100644
  1 +import React from "react";
  2 +import { Table } from "antd";
  3 +import { observer } from "mobx-react-lite";
  4 +import * as API from "../api";
  5 +import useInitial from "@/hooks/useInitail";
  6 +import ModifiedTableCell from "./ModifiedTableCell";
  7 +
  8 +const { Column } = Table;
  9 +
  10 +// 查看销顾任务弹框
  11 +interface AdviserTaskPreviewProps {
  12 + params: any; // API.PreviewTaskReq
  13 + showSeriesModal: (record: API.TaskListItem) => void;
  14 +}
  15 +
  16 +const AdviserTaskPreview = ({
  17 + params,
  18 + showSeriesModal,
  19 +}: AdviserTaskPreviewProps) => {
  20 + const { data, loading } = useInitial<API.PreviewTaskRes, API.PreviewTaskReq>(
  21 + API.previewTask,
  22 + {} as API.PreviewTaskRes,
  23 + params
  24 + );
  25 +
  26 + // 查看车系任务
  27 + const handlePreviewSeriesTask = (record: API.TaskListItem) => {
  28 + if (record.seriesTaskCount === 0) {
  29 + return;
  30 + }
  31 + showSeriesModal(record);
  32 + };
  33 +
  34 + return (
  35 + <Table
  36 + rowKey="dataId"
  37 + loading={loading}
  38 + dataSource={data.taskList}
  39 + pagination={false}
  40 + scroll={{ y: 450 }}
  41 + >
  42 + <Column title="姓名" dataIndex="dataName" />
  43 + <Column
  44 + title="零售任务(台)"
  45 + dataIndex="taskCount"
  46 + render={(text: string, record: API.TaskListItem) => {
  47 + return ModifiedTableCell(record, "taskCount");
  48 + }}
  49 + />
  50 + <Column
  51 + title="新能源车任务(台)"
  52 + dataIndex="newEnergyTaskCount"
  53 + render={(text: string, record: API.TaskListItem) => {
  54 + return ModifiedTableCell(record, "newEnergyTaskCount");
  55 + }}
  56 + />
  57 + <Column
  58 + title="传统燃油车任务(台)"
  59 + dataIndex="fuelVehicleTaskCount"
  60 + render={(text: string, record: API.TaskListItem) => {
  61 + return ModifiedTableCell(record, "fuelVehicleTaskCount");
  62 + }}
  63 + />
  64 + <Column
  65 + title="车辆毛利任务(元)"
  66 + dataIndex="vehicleGrossProfitTask"
  67 + render={(text: string, record: API.TaskListItem) => {
  68 + return ModifiedTableCell(record, "vehicleGrossProfitTask");
  69 + }}
  70 + />
  71 + <Column
  72 + title="线索到店零售台数(台)"
  73 + dataIndex="clueDealTaskCount"
  74 + render={(text: string, record: API.TaskListItem) => {
  75 + return ModifiedTableCell(record, "clueDealTaskCount");
  76 + }}
  77 + />
  78 + <Column
  79 + title="首客试驾成交任务数(台)"
  80 + dataIndex="testDriveTaskCount"
  81 + render={(text: string, record: API.TaskListItem) => {
  82 + return ModifiedTableCell(record, "testDriveTaskCount");
  83 + }}
  84 + />
  85 + <Column
  86 + title="攻坚车任务数(台)"
  87 + dataIndex="tackCarTaskCount"
  88 + render={(text: string, record: API.TaskListItem) => {
  89 + return ModifiedTableCell(record, "tackCarTaskCount");
  90 + }}
  91 + />
  92 + <Column
  93 + title="车系任务数(台)"
  94 + dataIndex="seriesTaskCount"
  95 + render={(text: string, record: API.TaskListItem) => {
  96 + if (record.dataId === -999) return text;
  97 + return <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>;
  98 + }}
  99 + />
  100 + </Table>
  101 + );
  102 +};
  103 +
  104 +export default observer(AdviserTaskPreview);
... ...
src/pages/order3/SaleTask/components/ApprovalProgressModal.tsx 0 → 100644
  1 +import ApprovalProgress from "@/components/ApprovalProgress";
  2 +import { Button, Modal } from "antd";
  3 +import React from "react";
  4 +import { useStore } from "../store";
  5 +
  6 +export default function ApprovalProgressModal() {
  7 + const { approvalProgressModalInfo, setApprovalProgressModalInfo } =
  8 + useStore();
  9 +
  10 + const onCancel = () => setApprovalProgressModalInfo({ visible: false });
  11 +
  12 + return (
  13 + <Modal
  14 + title={`${
  15 + approvalProgressModalInfo.title
  16 + ? approvalProgressModalInfo.title + "-"
  17 + : ""
  18 + }审批进度`}
  19 + open={approvalProgressModalInfo.visible}
  20 + onCancel={onCancel}
  21 + maskClosable={false}
  22 + destroyOnClose
  23 + footer={[
  24 + <Button key="cancel" onClick={onCancel}>
  25 + 关闭
  26 + </Button>,
  27 + ]}
  28 + >
  29 + <ApprovalProgress orderNo={approvalProgressModalInfo.orderNo} />
  30 + </Modal>
  31 + );
  32 +}
... ...
src/pages/order3/SaleTask/components/EntryTaskPreview.tsx 0 → 100644
  1 +import React, { useState } from "react";
  2 +import { Card, Radio, RadioChangeEvent, Row, Table } from "antd";
  3 +import { observer } from "mobx-react-lite";
  4 +import * as API from "../api";
  5 +import { OrderTaskApprovalType } from "../entity";
  6 +import useInitial from "@/hooks/useInitail";
  7 +import ModifiedTableCell from "./ModifiedTableCell";
  8 +
  9 +const { Column } = Table;
  10 +const RadioButton = Radio.Button;
  11 +const RadioGroup = Radio.Group;
  12 +
  13 +// 预览任务入口弹框
  14 +interface EntryTaskPreviewProps {
  15 + params: any; // API.PreviewTaskReq
  16 + showAdviserModal: (
  17 + record: API.TaskListItem,
  18 + type: OrderTaskApprovalType
  19 + ) => void;
  20 + showSeriesModal: (
  21 + record: API.TaskListItem,
  22 + type: OrderTaskApprovalType
  23 + ) => void;
  24 +}
  25 +
  26 +const EntryTaskPreview = ({
  27 + params,
  28 + showAdviserModal,
  29 + showSeriesModal,
  30 +}: EntryTaskPreviewProps) => {
  31 + const [type, setType] = useState(OrderTaskApprovalType.门店维度);
  32 +
  33 + const { data, loading, setParams } = useInitial<
  34 + API.PreviewTaskRes,
  35 + API.PreviewTaskReq
  36 + >(API.previewTask, {} as API.PreviewTaskRes, params);
  37 +
  38 + const handleChangeType = (e: RadioChangeEvent) => {
  39 + const value = e.target.value;
  40 + if (value === 99) {
  41 + setType(OrderTaskApprovalType.新车一级管理维度);
  42 + setParams(
  43 + // @ts-ignore
  44 + { orderTaskApprovalType: OrderTaskApprovalType.新车一级管理维度 },
  45 + true
  46 + );
  47 + return;
  48 + }
  49 + setType(value);
  50 + // @ts-ignore
  51 + setParams({ orderTaskApprovalType: value }, true);
  52 + };
  53 +
  54 + // 查看顾问任务
  55 + const handlePreviewAdviserTask = (record: API.TaskListItem) => {
  56 + if (record.dataId === -999) {
  57 + return;
  58 + }
  59 + showAdviserModal(record, type);
  60 + };
  61 +
  62 + // 查看车系任务
  63 + const handlePreviewSeriesTask = (record: API.TaskListItem) => {
  64 + if (record.seriesTaskCount === 0) {
  65 + return;
  66 + }
  67 + showSeriesModal(record, type);
  68 + };
  69 +
  70 + return (
  71 + <Card
  72 + title={
  73 + <Row align="middle" justify="start">
  74 + <RadioGroup onChange={handleChangeType} value={type}>
  75 + <RadioButton value={OrderTaskApprovalType.门店维度}>
  76 + 门店
  77 + </RadioButton>
  78 + <RadioButton value={OrderTaskApprovalType.销售顾问维度}>
  79 + 销顾
  80 + </RadioButton>
  81 + <RadioButton value={99}>销售管理</RadioButton>
  82 + </RadioGroup>
  83 + {type !== OrderTaskApprovalType.门店维度 &&
  84 + type !== OrderTaskApprovalType.销售顾问维度 && (
  85 + <RadioGroup
  86 + onChange={handleChangeType}
  87 + value={type}
  88 + style={{ marginLeft: 20 }}
  89 + >
  90 + <RadioButton value={OrderTaskApprovalType.新车一级管理维度}>
  91 + 销售一级管理
  92 + </RadioButton>
  93 + <RadioButton value={OrderTaskApprovalType.新车二级管理维度}>
  94 + 销售二级管理
  95 + </RadioButton>
  96 + <RadioButton value={OrderTaskApprovalType.新车三级管理维度}>
  97 + 销售三级管理
  98 + </RadioButton>
  99 + </RadioGroup>
  100 + )}
  101 + </Row>
  102 + }
  103 + >
  104 + <Table
  105 + rowKey="dataId"
  106 + loading={loading}
  107 + dataSource={data.taskList}
  108 + pagination={false}
  109 + scroll={{ y: 450 }}
  110 + >
  111 + <Column
  112 + title={type === OrderTaskApprovalType.门店维度 ? "门店" : "姓名"}
  113 + dataIndex="dataName"
  114 + filterSearch
  115 + onFilter={(
  116 + value: string | number | boolean,
  117 + record: API.TaskListItem
  118 + ) => {
  119 + return record.dataName.startsWith(value.toString());
  120 + }}
  121 + />
  122 + <Column
  123 + title="零售任务(台)"
  124 + dataIndex="taskCount"
  125 + render={(text: string, record: API.TaskListItem) => {
  126 + return ModifiedTableCell(record, "taskCount");
  127 + }}
  128 + />
  129 + <Column
  130 + title="新能源车任务(台)"
  131 + dataIndex="newEnergyTaskCount"
  132 + render={(text: string, record: API.TaskListItem) => {
  133 + return ModifiedTableCell(record, "newEnergyTaskCount");
  134 + }}
  135 + />
  136 + <Column
  137 + title="传统燃油车任务(台)"
  138 + dataIndex="fuelVehicleTaskCount"
  139 + render={(text: string, record: API.TaskListItem) => {
  140 + return ModifiedTableCell(record, "fuelVehicleTaskCount");
  141 + }}
  142 + />
  143 + <Column
  144 + title="车辆毛利任务(元)"
  145 + dataIndex="vehicleGrossProfitTask"
  146 + render={(text: string, record: API.TaskListItem) => {
  147 + return ModifiedTableCell(record, "vehicleGrossProfitTask");
  148 + }}
  149 + />
  150 + <Column
  151 + title="线索到店零售台数(台)"
  152 + dataIndex="clueDealTaskCount"
  153 + render={(text: string, record: API.TaskListItem) => {
  154 + return ModifiedTableCell(record, "clueDealTaskCount");
  155 + }}
  156 + />
  157 + <Column
  158 + title="首客试驾成交任务数(台)"
  159 + dataIndex="testDriveTaskCount"
  160 + render={(text: string, record: API.TaskListItem) => {
  161 + return ModifiedTableCell(record, "testDriveTaskCount");
  162 + }}
  163 + />
  164 + <Column
  165 + title="攻坚车任务数(台)"
  166 + dataIndex="tackCarTaskCount"
  167 + render={(text: string, record: API.TaskListItem) => {
  168 + return ModifiedTableCell(record, "tackCarTaskCount");
  169 + }}
  170 + />
  171 + <Column
  172 + title="车系任务数(台)"
  173 + dataIndex="seriesTaskCount"
  174 + render={(text: string, record: API.TaskListItem) => {
  175 + if (record.dataId === -999) return text;
  176 + return (
  177 + <a onClick={() => handlePreviewSeriesTask(record)}>{text}</a>
  178 + );
  179 + }}
  180 + />
  181 + {type === OrderTaskApprovalType.门店维度 && (
  182 + <Column
  183 + title="销顾"
  184 + render={(text: string, record: API.TaskListItem) => {
  185 + if (record.dataId === -999) return "-";
  186 + return (
  187 + <a onClick={() => handlePreviewAdviserTask(record)}>查看</a>
  188 + );
  189 + }}
  190 + />
  191 + )}
  192 + </Table>
  193 + </Card>
  194 + );
  195 +};
  196 +
  197 +export default observer(EntryTaskPreview);
... ...
src/pages/order3/SaleTask/components/ModifiedTableCell.tsx 0 → 100644
  1 +import React from "react";
  2 +import { Row } from "antd";
  3 +import * as API from "../api";
  4 +
  5 +export default function ModifiedTableCell(
  6 + record: API.TaskListItem,
  7 + field: string
  8 +) {
  9 + const capitalized = field.charAt(0).toUpperCase() + field.slice(1);
  10 + const beforeName = `before${capitalized}`;
  11 + const afterName = `after${capitalized}`;
  12 + const isModifiedName = `${field}Modified`;
  13 + const isModified = record[isModifiedName];
  14 + return (
  15 + <Row align="middle" justify="center">
  16 + {!isModified ? (
  17 + <span>{record[beforeName]}</span>
  18 + ) : (
  19 + <>
  20 + <span style={{ color: "#CCCCCC" }}>{record[beforeName]}</span>
  21 + <span style={{ color: "#F4333C " }}>&rarr;</span>
  22 + <span style={{ color: "#858CA0" }}>{record[afterName]}</span>
  23 + </>
  24 + )}
  25 + </Row>
  26 + );
  27 +}
... ...
src/pages/order3/SaleTask/components/SeriesTaskPreview.tsx 0 → 100644
  1 +import React from "react";
  2 +import { Row, Table, Tag } from "antd";
  3 +import { observer } from "mobx-react-lite";
  4 +import * as API from "../api";
  5 +import useInitial from "@/hooks/useInitail";
  6 +
  7 +const { Column } = Table;
  8 +
  9 +// 查看车系任务弹框
  10 +interface SeriesTaskPreviewProps {
  11 + params: any; // API.PreviewTaskReq
  12 +}
  13 +
  14 +const SeriesTaskPreview = ({ params }: SeriesTaskPreviewProps) => {
  15 + const { data, loading } = useInitial<API.PreviewTaskRes, API.PreviewTaskReq>(
  16 + API.previewTask,
  17 + {} as API.PreviewTaskRes,
  18 + params
  19 + );
  20 +
  21 + return (
  22 + <Table
  23 + rowKey="dataId"
  24 + loading={loading}
  25 + dataSource={data.taskList}
  26 + pagination={false}
  27 + scroll={{ y: 450 }}
  28 + >
  29 + <Column
  30 + title="车系名称"
  31 + dataIndex="dataName"
  32 + render={(text: string, record: API.TaskListItem) => {
  33 + return (
  34 + <Row align="middle" justify="start">
  35 + <span>{text}</span>
  36 + {record.newEnergy && (
  37 + <Tag style={{ marginBottom: 5, marginLeft: 7 }} color="green">
  38 + 新能源
  39 + </Tag>
  40 + )}
  41 + </Row>
  42 + );
  43 + }}
  44 + />
  45 + <Column title="零售任务(台)" dataIndex="seriesTaskCount" />
  46 + </Table>
  47 + );
  48 +};
  49 +
  50 +export default observer(SeriesTaskPreview);
... ...
src/pages/order3/SaleTask/entity.ts 0 → 100644
  1 +export enum OrderTaskApprovalType {
  2 + 门店维度 = 1,
  3 + 销售顾问维度 = 2,
  4 + 新车一级管理维度 = 3,
  5 + 新车二级管理维度 = 4,
  6 + 新车三级管理维度 = 5,
  7 + 车系 = 6,
  8 +}
  9 +
  10 +export const MAX_NUM = 999999;
... ...
src/pages/order3/SaleTask/index.tsx 0 → 100644
  1 +import React, { useEffect, useState } from "react";
  2 +import {
  3 + Card,
  4 + Table,
  5 + DatePicker,
  6 + Row,
  7 + message,
  8 + Input,
  9 + Button,
  10 + Modal,
  11 +} from "antd";
  12 +import * as API from "./api";
  13 +import { PageHeaderWrapper } from "@ant-design/pro-layout";
  14 +import { history } from "umi";
  15 +import moment, { Moment } from "moment";
  16 +import useInitial from "@/hooks/useInitail";
  17 +import { Provider, useStore } from "./store";
  18 +import ApprovalProgressModal from "./components/ApprovalProgressModal";
  19 +import EntryTaskPreview from "./components/EntryTaskPreview";
  20 +import { OrderTaskApprovalType } from "./entity";
  21 +import AdviserTaskPreview from "./components/AdviserTaskPreview";
  22 +import SeriesTaskPreview from "./components/SeriesTaskPreview";
  23 +import ApproveModal from "@/pages/order3/Common/ApproveModal";
  24 +
  25 +const { Column } = Table;
  26 +
  27 +export default () => (
  28 + <Provider>
  29 + <SaleTaskList />
  30 + </Provider>
  31 +);
  32 +
  33 +function SaleTaskList() {
  34 + const {
  35 + isReadOnly,
  36 + setShopTaskItem,
  37 + setIsReadOnly,
  38 + setApprovalProgressModalInfo,
  39 + } = useStore();
  40 + const [approveOpen, setApproveOpen] = useState(false);
  41 + const [targetMonth, setTargetMonth] = useState(moment());
  42 + const [etpVisible, setEtpVisible] = useState(false);
  43 + const [previewTaskParams, setPreviewTaskParams] = useState({});
  44 + const [atpVisible, setAtpVisible] = useState(false);
  45 + const [adviserTaskParams, setAdviserTaskParams] = useState({});
  46 + const [stpVisible, setStpVisible] = useState(false);
  47 + const [seriesTaskParams, setSeriesTaskParams] = useState({});
  48 +
  49 + const { data, loading, setParams } = useInitial(
  50 + API.getSaleTaskApi,
  51 + {} as API.GetSaleTaskApiRes,
  52 + {
  53 + taskDate: targetMonth.valueOf(),
  54 + }
  55 + );
  56 +
  57 + useEffect(() => {
  58 + setIsReadOnly(!data.canModified);
  59 + }, [data]);
  60 +
  61 + const handleChangeMonth = (date: Moment | null) => {
  62 + setTargetMonth(moment(date));
  63 + setParams({ taskDate: moment(date).valueOf() }, true);
  64 + };
  65 +
  66 + const goToEditPage = (record: API.ShopTaskItem) => {
  67 + setShopTaskItem(record);
  68 + history.push({
  69 + pathname: "/order3/saleTask/edit",
  70 + query: {
  71 + readOnly: isReadOnly ? "1" : "0",
  72 + shopId: String(record.shopId),
  73 + taskDate: String(targetMonth.valueOf()),
  74 + },
  75 + });
  76 + };
  77 +
  78 + // 查看流程进度
  79 + const viewProcess = () => {
  80 + setApprovalProgressModalInfo({
  81 + visible: true,
  82 + title: `【${moment(targetMonth).format("YYYY-MM")}】月度零售任务审批进度`,
  83 + orderNo: data.approvalNumber,
  84 + });
  85 + };
  86 +
  87 + const cancelSaleTask = () => {
  88 + Modal.confirm({
  89 + title: "提示",
  90 + content: `确认撤销【${targetMonth.format("YYYY-MM")}】零售任务审批吗?`,
  91 + zIndex: 1002,
  92 + onOk: () => {
  93 + API.cancelSaleTask({
  94 + id: data.id,
  95 + })
  96 + .then((res) => {
  97 + message.success("撤销成功");
  98 + })
  99 + .catch((error: any) => {
  100 + message.error(error.message ?? "请求失败");
  101 + })
  102 + .finally(() => {
  103 + setParams({}, true);
  104 + });
  105 + },
  106 + });
  107 + };
  108 +
  109 + const submitSaleTask = (parmas: any) => {
  110 + const hide = message.loading("提交中", 0);
  111 + API.submitSaleTask({
  112 + id: data.id,
  113 + ...parmas,
  114 + })
  115 + .then((res) => {
  116 + message.success("提交成功");
  117 + })
  118 + .catch((error: any) => {
  119 + message.error(error.message ?? "请求失败");
  120 + })
  121 + .finally(() => {
  122 + hide();
  123 + setParams({}, true);
  124 + });
  125 + };
  126 +
  127 + const handlePreviewTask = () => {
  128 + const params: any = {
  129 + id: data.id,
  130 + orderTaskApprovalType: OrderTaskApprovalType.门店维度,
  131 + };
  132 + setPreviewTaskParams(params);
  133 + setEtpVisible(true);
  134 + };
  135 +
  136 + return (
  137 + <PageHeaderWrapper title="零售任务分配">
  138 + <Card>
  139 + <Row align="middle" justify="start" style={{ marginBottom: 20 }}>
  140 + <DatePicker
  141 + placeholder="月度"
  142 + style={{ width: 260 }}
  143 + picker="month"
  144 + value={targetMonth}
  145 + onChange={handleChangeMonth}
  146 + allowClear={false}
  147 + />
  148 + <Input.Search
  149 + allowClear
  150 + placeholder="门店名称"
  151 + style={{ width: 260, marginLeft: 15 }}
  152 + onSearch={(v) => {
  153 + setParams({ shopName: v }, true);
  154 + }}
  155 + />
  156 + </Row>
  157 + <Table
  158 + rowKey="id"
  159 + loading={loading}
  160 + dataSource={data.shopTaskList}
  161 + pagination={false}
  162 + scroll={{ y: 450 }}
  163 + summary={() => {
  164 + if (!data || !data.shopTaskList || data.shopTaskList.length <= 0) {
  165 + return null;
  166 + }
  167 + return (
  168 + <Table.Summary fixed="bottom">
  169 + <Table.Summary.Row
  170 + style={{
  171 + background: "#FAFAFA",
  172 + fontSize: 18,
  173 + fontWeight: 500,
  174 + }}
  175 + >
  176 + <Table.Summary.Cell align="left" index={1}>
  177 + 合计
  178 + </Table.Summary.Cell>
  179 + <Table.Summary.Cell align="left" index={2}>
  180 + {data.totalTaskCount}
  181 + </Table.Summary.Cell>
  182 + <Table.Summary.Cell align="left" index={3}>
  183 + {data.newEnergyTaskCount}
  184 + </Table.Summary.Cell>
  185 + <Table.Summary.Cell align="left" index={4}>
  186 + {data.fuelVehicleTaskCount}
  187 + </Table.Summary.Cell>
  188 + <Table.Summary.Cell align="left" index={5}>
  189 + {data.vehicleGrossProfitTask}
  190 + </Table.Summary.Cell>
  191 + <Table.Summary.Cell index={6}>
  192 + {data.clueDealTaskCount}
  193 + </Table.Summary.Cell>
  194 + <Table.Summary.Cell index={7}>
  195 + {data.testDriveTaskCount}
  196 + </Table.Summary.Cell>
  197 + <Table.Summary.Cell index={8}>
  198 + {data.tackCarTaskCount}
  199 + </Table.Summary.Cell>
  200 + <Table.Summary.Cell index={9}>
  201 + {data.seriesTaskCount}
  202 + </Table.Summary.Cell>
  203 + <Table.Summary.Cell index={10} />
  204 + </Table.Summary.Row>
  205 + </Table.Summary>
  206 + );
  207 + }}
  208 + >
  209 + <Column title="门店" dataIndex="shopName" />
  210 + <Column title="零售任务(台)" dataIndex="taskCount" />
  211 + <Column title="新能源车任务(台)" dataIndex="newEnergyTaskCount" />
  212 + <Column title="传统燃油车任务(台)" dataIndex="fuelVehicleTaskCount" />
  213 + <Column title="车辆毛利任务(元)" dataIndex="vehicleGrossProfitTask" />
  214 + <Column title="线索到店零售台数(台)" dataIndex="clueDealTaskCount" />
  215 + <Column
  216 + title="首客试驾成交任务数(台)"
  217 + dataIndex="testDriveTaskCount"
  218 + />
  219 + <Column title="攻坚车任务数(台)" dataIndex="tackCarTaskCount" />
  220 + <Column title="车系任务数(台)" dataIndex="seriesTaskCount" />
  221 + <Column
  222 + title="操作"
  223 + render={(text: string, record: API.ShopTaskItem) => {
  224 + return (
  225 + <a
  226 + onClick={() => {
  227 + goToEditPage(record);
  228 + }}
  229 + >
  230 + {isReadOnly ? "查看" : "编辑"}
  231 + </a>
  232 + );
  233 + }}
  234 + />
  235 + </Table>
  236 + {data.revoke ? (
  237 + <Row align="middle" justify="center" style={{ marginTop: 50 }}>
  238 + <Button onClick={cancelSaleTask}>撤销</Button>
  239 + <Button
  240 + type="primary"
  241 + style={{ marginLeft: 10 }}
  242 + onClick={viewProcess}
  243 + >
  244 + 审批进度
  245 + </Button>
  246 + <Button
  247 + type="primary"
  248 + style={{ marginLeft: 10 }}
  249 + onClick={handlePreviewTask}
  250 + >
  251 + 预览任务
  252 + </Button>
  253 + </Row>
  254 + ) : (
  255 + !isReadOnly && (
  256 + <Row align="middle" justify="center" style={{ marginTop: 50 }}>
  257 + <Button type="primary" onClick={() => setApproveOpen(true)}>
  258 + 提交审批
  259 + </Button>
  260 + </Row>
  261 + )
  262 + )}
  263 + </Card>
  264 + <Modal
  265 + width={1200}
  266 + title="预览任务"
  267 + open={etpVisible}
  268 + onCancel={() => setEtpVisible(false)}
  269 + destroyOnClose
  270 + footer={null}
  271 + >
  272 + <Row align="middle" justify="start" style={{ marginBottom: 20 }}>
  273 + <DatePicker
  274 + placeholder="月度"
  275 + style={{ width: 230 }}
  276 + picker="month"
  277 + value={targetMonth}
  278 + allowClear={false}
  279 + disabled
  280 + />
  281 + {/* <Input.Search
  282 + allowClear
  283 + placeholder="门店名称"
  284 + style={{ width: 263, marginLeft: 20 }}
  285 + onSearch={(v) => {
  286 + // todo setParams({ shopName: v }, true);
  287 + }}
  288 + /> */}
  289 + </Row>
  290 + <EntryTaskPreview
  291 + params={previewTaskParams}
  292 + showAdviserModal={(record, type) => {
  293 + const params: any = {
  294 + id: data.id,
  295 + taskId: record.id,
  296 + orderTaskApprovalType: OrderTaskApprovalType.门店维度, // 只有门店有查看销顾任务
  297 + };
  298 + switch (type) {
  299 + case OrderTaskApprovalType.门店维度:
  300 + params.shopId = record.dataId;
  301 + break;
  302 + case OrderTaskApprovalType.销售顾问维度:
  303 + params.staffId = record.dataId;
  304 + break;
  305 + case OrderTaskApprovalType.新车一级管理维度:
  306 + params.firstManageId = record.dataId;
  307 + break;
  308 + case OrderTaskApprovalType.新车二级管理维度:
  309 + params.secondManageId = record.dataId;
  310 + break;
  311 + case OrderTaskApprovalType.新车三级管理维度:
  312 + params.thirdManageId = record.dataId;
  313 + break;
  314 + default:
  315 + break;
  316 + }
  317 + setAdviserTaskParams(params);
  318 + setAtpVisible(true);
  319 + }}
  320 + showSeriesModal={(record, type) => {
  321 + const params: any = {
  322 + id: data.id,
  323 + taskId: record.id,
  324 + orderTaskApprovalType: OrderTaskApprovalType.车系,
  325 + };
  326 + switch (type) {
  327 + case OrderTaskApprovalType.门店维度:
  328 + params.shopId = record.dataId;
  329 + break;
  330 + case OrderTaskApprovalType.销售顾问维度:
  331 + params.staffId = record.dataId;
  332 + break;
  333 + case OrderTaskApprovalType.新车一级管理维度:
  334 + params.firstManageId = record.dataId;
  335 + break;
  336 + case OrderTaskApprovalType.新车二级管理维度:
  337 + params.secondManageId = record.dataId;
  338 + break;
  339 + case OrderTaskApprovalType.新车三级管理维度:
  340 + params.thirdManageId = record.dataId;
  341 + break;
  342 + default:
  343 + break;
  344 + }
  345 + setSeriesTaskParams(params);
  346 + setStpVisible(true);
  347 + }}
  348 + />
  349 + </Modal>
  350 + <Modal
  351 + width={1200}
  352 + title="销顾任务"
  353 + open={atpVisible}
  354 + onCancel={() => setAtpVisible(false)}
  355 + destroyOnClose
  356 + footer={null}
  357 + >
  358 + <AdviserTaskPreview
  359 + params={adviserTaskParams}
  360 + showSeriesModal={(record) => {
  361 + const params = { ...adviserTaskParams } as any;
  362 + params.taskId = record.id;
  363 + params.orderTaskApprovalType = OrderTaskApprovalType.车系;
  364 + params.staffId = record.dataId;
  365 + setSeriesTaskParams(params);
  366 + setStpVisible(true);
  367 + }}
  368 + />
  369 + </Modal>
  370 + <Modal
  371 + width={600}
  372 + title="车系任务"
  373 + open={stpVisible}
  374 + onCancel={() => setStpVisible(false)}
  375 + destroyOnClose
  376 + footer={null}
  377 + >
  378 + <SeriesTaskPreview params={seriesTaskParams} />
  379 + </Modal>
  380 + <ApprovalProgressModal />
  381 + <ApproveModal
  382 + callback={submitSaleTask}
  383 + open={approveOpen}
  384 + setOpen={setApproveOpen}
  385 + />
  386 + </PageHeaderWrapper>
  387 + );
  388 +}
... ...
src/pages/order3/SaleTask/store.ts 0 → 100644
  1 +import { useState } from "react";
  2 +import * as API from "./api";
  3 +import { createStore } from "@/hooks/moz";
  4 +
  5 +function store() {
  6 + const [approvalProgressModalInfo, setApprovalProgressModalInfo] =
  7 + useState<PublicNotice.ApprovalProgressModalInfo>({ visible: false });
  8 + const [shopTaskItem, setShopTaskItem] = useState<API.ShopTaskItem | null>(
  9 + null
  10 + );
  11 + const [isReadOnly, setIsReadOnly] = useState(false);
  12 + const [editAdviser, setEditAdviser] = useState<API.StaffTaskItem>();
  13 +
  14 + /** -----------门店零售任务分配----------- */
  15 + // 更新车系任务
  16 + const setShopSeriesRow = (seriesId: number, row: API.SeriesTaskItem) => {
  17 + const newTask = { ...shopTaskItem! };
  18 + const editIndex = newTask.seriesTaskList.findIndex(
  19 + (item) => seriesId === item.seriesId
  20 + );
  21 + const item = newTask.seriesTaskList[editIndex];
  22 + newTask.seriesTaskList.splice(editIndex, 1, {
  23 + ...item,
  24 + ...row,
  25 + });
  26 + setShopTaskItem(newTask);
  27 + };
  28 +
  29 + // 删除车系任务
  30 + const deleteShopSeriesRow = (seriesId: number) => {
  31 + const newTask = { ...shopTaskItem! };
  32 + const deleteIndex = newTask.seriesTaskList.findIndex(
  33 + (item) => seriesId === item.seriesId
  34 + );
  35 + newTask.seriesTaskList.splice(deleteIndex, 1);
  36 + setShopTaskItem(newTask);
  37 + };
  38 +
  39 + // 添加车系任务
  40 + const addShopSeriesRow = (values: any) => {
  41 + const newTask = { ...shopTaskItem! };
  42 + const selectedSeries = values.series.map((item: any) => ({
  43 + brandId: values.brand.value,
  44 + brandName: values.brand.label,
  45 + seriesId: item.value,
  46 + seriesName: item.label,
  47 + newEnergy: item.newEnergy,
  48 + taskCount: 0,
  49 + }));
  50 + newTask.seriesTaskList = selectedSeries.concat(newTask.seriesTaskList);
  51 + setShopTaskItem(newTask);
  52 + };
  53 +
  54 + /** -----------销顾零售任务分配----------- */
  55 + // 更新车系任务
  56 + const setAdviserSeriesRow = (
  57 + seriesId: number,
  58 + row: API.SeriesTaskItem
  59 + ) => {
  60 + const newAdviser = { ...editAdviser! };
  61 + const editIndex = newAdviser.seriesTaskList.findIndex(
  62 + (item) => item.seriesId === seriesId
  63 + );
  64 + const item = newAdviser.seriesTaskList[editIndex];
  65 + newAdviser.seriesTaskList.splice(editIndex, 1, {
  66 + ...item,
  67 + ...row,
  68 + });
  69 + setEditAdviser(newAdviser);
  70 + };
  71 +
  72 + // 删除车系任务
  73 + const deleteAdviserSeriesRow = (seriesId: number) => {
  74 + const newAdviser = { ...editAdviser! };
  75 + const deleteIndex = newAdviser.seriesTaskList.findIndex(
  76 + (item) => item.seriesId === seriesId
  77 + );
  78 + newAdviser.seriesTaskList.splice(deleteIndex, 1);
  79 + setEditAdviser(newAdviser);
  80 + };
  81 +
  82 + // 添加车系任务
  83 + const addAdviserSeriesRow = (values: any) => {
  84 + const newAdviser = { ...editAdviser! };
  85 + const selectedSeries = values.series.map((item: any) => ({
  86 + brandId: values.brand.value,
  87 + brandName: values.brand.label,
  88 + seriesId: item.value,
  89 + seriesName: item.label,
  90 + newEnergy: item.newEnergy,
  91 + taskCount: 0,
  92 + }));
  93 + newAdviser.seriesTaskList = selectedSeries.concat(
  94 + newAdviser.seriesTaskList
  95 + );
  96 + setEditAdviser(newAdviser);
  97 + };
  98 +
  99 + return {
  100 + approvalProgressModalInfo,
  101 + shopTaskItem,
  102 + isReadOnly,
  103 + editAdviser,
  104 + setEditAdviser,
  105 + setApprovalProgressModalInfo,
  106 + setShopTaskItem,
  107 + setIsReadOnly,
  108 + // 业务函数
  109 + setShopSeriesRow,
  110 + deleteShopSeriesRow,
  111 + addShopSeriesRow,
  112 + setAdviserSeriesRow,
  113 + deleteAdviserSeriesRow,
  114 + addAdviserSeriesRow,
  115 + };
  116 +}
  117 +
  118 +export const { Provider, useStore } = createStore(store);
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/components/AdviserTask.tsx 0 → 100644
  1 +import React, { useEffect, useRef, useState } from "react";
  2 +import { Table, Row, Input, Modal, Button, message } from "antd";
  3 +import { useStore } from "../../../store";
  4 +import * as API from "../../../api";
  5 +import AdviserTaskEdit from "./AdviserTaskEdit";
  6 +import { history, useRequest } from "umi";
  7 +import { cloneDeep } from "lodash";
  8 +
  9 +const { Column } = Table;
  10 +
  11 +interface AdviserTaskEditForm {
  12 + submit: (callback: (data: any) => void) => void;
  13 +}
  14 +
  15 +interface AdviserTaskProps {
  16 + form: any;
  17 +}
  18 +
  19 +export default function AdviserTask({ form }: AdviserTaskProps) {
  20 + const adviserTaskEditRef = useRef<AdviserTaskEditForm>(null);
  21 + const {
  22 + shopTaskItem,
  23 + isReadOnly,
  24 + setShopTaskItem,
  25 + editAdviser,
  26 + setEditAdviser,
  27 + } = useStore();
  28 + const [advisersFiltered, setAdvisersFiltered] = useState<API.StaffTaskItem[]>(
  29 + []
  30 + );
  31 + const [visible, setVisible] = useState(false);
  32 + const saveShopSaleTaskHook = useRequest(API.saveShopSaleTask, {
  33 + manual: true,
  34 + throwOnError: true,
  35 + });
  36 +
  37 + // 用于顾问前端搜索
  38 + useEffect(() => {
  39 + setAdvisersFiltered(shopTaskItem?.staffTaskList ?? []);
  40 + }, [shopTaskItem]);
  41 +
  42 + const filterByAdviserName = (value: string) => {
  43 + const list = (shopTaskItem?.staffTaskList ?? []).filter(
  44 + (item: any) => item.staffName.indexOf(value) !== -1
  45 + );
  46 + setAdvisersFiltered(list);
  47 + };
  48 +
  49 + // 前端更新编辑后的顾问分配任务
  50 + const onOk = () => {
  51 + adviserTaskEditRef.current?.submit((newTask) => {
  52 + setShopTaskItem(newTask);
  53 + setAdvisersFiltered(newTask?.staffTaskList); // 刷新列表
  54 + setVisible(false);
  55 + });
  56 + };
  57 +
  58 + const handleGoBack = () => {
  59 + history.goBack(); // todo 提示是否有未保存的修改
  60 + };
  61 +
  62 + const handleSaveTask = async () => {
  63 + const shopFormValue = form.getFieldsValue();
  64 + const { taskId, ...other } = shopTaskItem!; // 因为门店零售任务分配表格数据填写时未修改 store
  65 + saveShopSaleTaskHook
  66 + .run({ ...other, id: taskId, ...shopFormValue })
  67 + .then(() => {
  68 + message.success("保存草稿成功");
  69 + })
  70 + .catch((error: any) => {
  71 + message.error(error.message ?? "请求失败");
  72 + });
  73 + };
  74 +
  75 + return (
  76 + <>
  77 + <Row
  78 + align="middle"
  79 + justify="start"
  80 + style={{ marginBottom: 20, marginTop: 14 }}
  81 + >
  82 + <Input.Search
  83 + allowClear
  84 + placeholder="顾问名称"
  85 + style={{ width: 260, marginLeft: 15 }}
  86 + onSearch={filterByAdviserName}
  87 + />
  88 + </Row>
  89 + <Table
  90 + dataSource={[...advisersFiltered]}
  91 + pagination={false}
  92 + rowKey="id"
  93 + loading={false}
  94 + >
  95 + <Column title="销售顾问" dataIndex="staffName" width={100} />
  96 + <Column title="零售任务(台)" dataIndex="taskCount" />
  97 + <Column title="新能源车任务(台)" dataIndex="newEnergyTaskCount" />
  98 + <Column title="传统燃油车任务(台)" dataIndex="fuelVehicleTaskCount" />
  99 + <Column title="车辆毛利任务(元)" dataIndex="vehicleGrossProfitTask" />
  100 + <Column title="线索到店零售台数(台)" dataIndex="clueDealTaskCount" />
  101 + <Column title="攻坚车任务数(台)" dataIndex="tackCarTaskCount" />
  102 + <Column title="首客试驾成交任务数(台)" dataIndex="testDriveTaskCount" />
  103 + <Column title="车系任务数(台)" dataIndex="seriesTaskCount" />
  104 + {!isReadOnly && (
  105 + <Column
  106 + title="操作"
  107 + width={100}
  108 + render={(text: string, record: API.StaffTaskItem) => {
  109 + return (
  110 + <a
  111 + onClick={() => {
  112 + setEditAdviser(cloneDeep(record)); // 注意对象引用
  113 + setVisible(true);
  114 + }}
  115 + >
  116 + 编辑
  117 + </a>
  118 + );
  119 + }}
  120 + />
  121 + )}
  122 + </Table>
  123 + <Row align="middle" justify="center" style={{ marginTop: 50 }}>
  124 + <Button onClick={handleGoBack}>返回列表</Button>
  125 + {!isReadOnly && (
  126 + <Button
  127 + type="primary"
  128 + style={{ marginLeft: 10 }}
  129 + loading={saveShopSaleTaskHook.loading}
  130 + onClick={handleSaveTask}
  131 + >
  132 + 保存草稿
  133 + </Button>
  134 + )}
  135 + </Row>
  136 + <Modal
  137 + width={1200}
  138 + title={`当前选择顾问:${editAdviser?.staffName}`}
  139 + open={visible}
  140 + onOk={onOk}
  141 + okText="确认"
  142 + onCancel={() => setVisible(false)}
  143 + maskClosable={false}
  144 + keyboard={false}
  145 + destroyOnClose
  146 + >
  147 + {/* @ts-ignore */}
  148 + <AdviserTaskEdit ref={adviserTaskEditRef} />
  149 + </Modal>
  150 + </>
  151 + );
  152 +}
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/components/AdviserTaskEdit.tsx 0 → 100644
  1 +import React, {
  2 + useEffect,
  3 + useImperativeHandle,
  4 + useMemo,
  5 + useState,
  6 +} from "react";
  7 +import {
  8 + InputNumber,
  9 + Divider,
  10 + Form,
  11 + message,
  12 + Table,
  13 + Button,
  14 + Row,
  15 + Tag,
  16 +} from "antd";
  17 +import { PlusOutlined } from "@ant-design/icons";
  18 +import * as API from "../../../api";
  19 +import styles from "../index.less";
  20 +import { useStore } from "../../../store";
  21 +import _ from "lodash";
  22 +import EditableCell from "./EditableCell";
  23 +import SeriesModal from "./SeriesModal";
  24 +import { observer } from "mobx-react-lite";
  25 +import { MAX_NUM } from "../../../entity";
  26 +
  27 +let _callback: (data: any) => void;
  28 +const { Column } = Table;
  29 +
  30 +export interface AdviserTaskEditProps {}
  31 +
  32 +function AdviserTaskEdit(props: AdviserTaskEditProps, ref: any) {
  33 + const {
  34 + shopTaskItem,
  35 + isReadOnly,
  36 + setAdviserSeriesRow,
  37 + deleteAdviserSeriesRow,
  38 + addAdviserSeriesRow,
  39 + editAdviser,
  40 + } = useStore();
  41 + const [form] = Form.useForm();
  42 + const [seriesForm] = Form.useForm();
  43 + const [editingKey, setEditingKey] = useState(-1); // 编辑表格 key
  44 + const [seriesVisible, setSeriesVisible] = useState(false);
  45 +
  46 + // 车系去重
  47 + const selectedIds = useMemo(() => {
  48 + if (!editAdviser) return [];
  49 + return editAdviser?.seriesTaskList.map((item) => String(item.seriesId));
  50 + }, [editAdviser]);
  51 +
  52 + // 计算车系任务总数自动填写
  53 + useEffect(() => {
  54 + const total = editAdviser?.seriesTaskList.reduce(
  55 + (total, currItem) => total + currItem.taskCount,
  56 + 0
  57 + );
  58 + form.setFieldValue("seriesTaskCount", total);
  59 + }, [editAdviser]);
  60 +
  61 + useImperativeHandle(ref, () => {
  62 + return {
  63 + submit: (callback: (data: any) => void) => {
  64 + _callback = callback;
  65 + form.submit();
  66 + },
  67 + };
  68 + });
  69 +
  70 + const isEditing = (record: API.SeriesTaskItem) => {
  71 + return record.seriesId === Number(editingKey);
  72 + };
  73 +
  74 + const setSeriesRow = (seriesId: number) => {
  75 + seriesForm
  76 + .validateFields()
  77 + .then((row: any) => {
  78 + setEditingKey(-1);
  79 + setAdviserSeriesRow(seriesId, row);
  80 + })
  81 + .catch((error: any) => {
  82 + message.error(error.message ?? "表单校验失败");
  83 + });
  84 + };
  85 +
  86 + const editSeriesRow = (record: API.SeriesTaskItem) => {
  87 + seriesForm.setFieldsValue({ ...record });
  88 + setEditingKey(record.seriesId);
  89 + };
  90 +
  91 + // 添加车系
  92 + const handleSelectSeries = (values: any) => {
  93 + addAdviserSeriesRow(values);
  94 + setSeriesVisible(false);
  95 + };
  96 +
  97 + const onFinish = async (values: any) => {
  98 + await form.validateFields();
  99 + const newTask = { ...shopTaskItem! };
  100 + const currAdviserIndex = newTask.staffTaskList.findIndex(
  101 + (item) => item.staffId === editAdviser?.staffId
  102 + );
  103 + newTask.staffTaskList[currAdviserIndex] = { ...editAdviser, ...values };
  104 + _callback(newTask);
  105 + };
  106 +
  107 + const layout = {
  108 + labelCol: { span: 5 },
  109 + wrapperCol: { span: 19 },
  110 + };
  111 +
  112 + const components = {
  113 + body: {
  114 + cell: EditableCell,
  115 + },
  116 + };
  117 +
  118 + return (
  119 + <div style={{ width: 1050, margin: "0 auto" }}>
  120 + <div style={{ width: 950, margin: "0 auto", paddingTop: 40 }}>
  121 + <Form
  122 + {...layout}
  123 + labelAlign="left"
  124 + form={form}
  125 + initialValues={editAdviser}
  126 + onFinish={onFinish}
  127 + >
  128 + <Form.Item name="taskCount" label="零售任务:" required>
  129 + <InputNumber
  130 + formatter={(value) => `${value}台`}
  131 + parser={(value: any) => value.replace("台", "")}
  132 + min={0}
  133 + max={MAX_NUM}
  134 + style={{ width: "100%" }}
  135 + disabled={isReadOnly}
  136 + precision={0}
  137 + />
  138 + </Form.Item>
  139 + <Form.Item
  140 + noStyle
  141 + shouldUpdate={(prevValues, currentValues) => {
  142 + if (prevValues.taskCount !== currentValues.taskCount) {
  143 + form.setFieldValue(
  144 + "clueDealTaskCount",
  145 + (
  146 + currentValues.taskCount *
  147 + ((shopTaskItem?.clueDealTaskRate ?? 100) / 100)
  148 + ).toFixed(2)
  149 + );
  150 + }
  151 + return prevValues.taskCount !== currentValues.taskCount;
  152 + }}
  153 + >
  154 + {() => {
  155 + return (
  156 + <Form.Item
  157 + name="clueDealTaskCount"
  158 + label="线索到店零售台数:"
  159 + required
  160 + >
  161 + <InputNumber
  162 + formatter={(value) => `${value}台`}
  163 + parser={(value: any) => value.replace("台", "")}
  164 + min={0}
  165 + max={MAX_NUM}
  166 + style={{ width: "100%" }}
  167 + disabled={isReadOnly}
  168 + precision={2}
  169 + />
  170 + </Form.Item>
  171 + );
  172 + }}
  173 + </Form.Item>
  174 + <Form.Item
  175 + name="newEnergyTaskCount"
  176 + label="新能源车任务台数:"
  177 + dependencies={["taskCount", "fuelVehicleTaskCount"]}
  178 + required
  179 + rules={[
  180 + ({ getFieldValue }) => ({
  181 + validator(_, value) {
  182 + const taskCount = getFieldValue("taskCount");
  183 + const fuelVehicleTaskCount = getFieldValue(
  184 + "fuelVehicleTaskCount"
  185 + );
  186 + if (
  187 + value >= 0 &&
  188 + fuelVehicleTaskCount + value === taskCount
  189 + ) {
  190 + return Promise.resolve();
  191 + }
  192 + return Promise.reject(
  193 + new Error(
  194 + "规则:新能源车任务台数 + 传统燃油车任务台数 = 零售任务台数"
  195 + )
  196 + );
  197 + },
  198 + }),
  199 + ]}
  200 + >
  201 + <InputNumber
  202 + formatter={(value) => `${value}台`}
  203 + parser={(value: any) => value.replace("台", "")}
  204 + min={0}
  205 + max={MAX_NUM}
  206 + style={{ width: "100%" }}
  207 + disabled={isReadOnly}
  208 + precision={0}
  209 + />
  210 + </Form.Item>
  211 + <Form.Item
  212 + name="fuelVehicleTaskCount"
  213 + label="传统燃油车任务台数:"
  214 + required
  215 + dependencies={["taskCount", "newEnergyTaskCount"]}
  216 + rules={[
  217 + ({ getFieldValue }) => ({
  218 + validator(_, value) {
  219 + const taskCount = getFieldValue("taskCount");
  220 + const newEnergyTaskCount =
  221 + getFieldValue("newEnergyTaskCount");
  222 + if (value >= 0 && newEnergyTaskCount + value === taskCount) {
  223 + return Promise.resolve();
  224 + }
  225 + return Promise.reject(
  226 + new Error(
  227 + "规则:新能源车任务台数 + 传统燃油车任务台数 = 零售任务台数"
  228 + )
  229 + );
  230 + },
  231 + }),
  232 + ]}
  233 + >
  234 + <InputNumber
  235 + formatter={(value) => `${value}台`}
  236 + parser={(value: any) => value.replace("台", "")}
  237 + min={0}
  238 + max={MAX_NUM}
  239 + style={{ width: "100%" }}
  240 + disabled={isReadOnly}
  241 + precision={0}
  242 + />
  243 + </Form.Item>
  244 + <Form.Item
  245 + name="vehicleGrossProfitTask"
  246 + label="车辆毛利任务:"
  247 + required
  248 + >
  249 + <InputNumber
  250 + formatter={(value) => `${value}元`}
  251 + parser={(value: any) => value.replace("元", "")}
  252 + min={0}
  253 + max={MAX_NUM}
  254 + style={{ width: "100%" }}
  255 + disabled={isReadOnly}
  256 + precision={2}
  257 + />
  258 + </Form.Item>
  259 + <Form.Item
  260 + name="testDriveTaskCount"
  261 + label="首客试驾成交任务台数:"
  262 + required
  263 + >
  264 + <InputNumber
  265 + formatter={(value) => `${value}台`}
  266 + parser={(value: any) => value.replace("台", "")}
  267 + min={0}
  268 + max={MAX_NUM}
  269 + style={{ width: "100%" }}
  270 + disabled={isReadOnly}
  271 + precision={0}
  272 + />
  273 + </Form.Item>
  274 + <Form.Item name="tackCarTaskCount" label="攻坚车任务台数:" required>
  275 + <InputNumber
  276 + formatter={(value) => `${value}台`}
  277 + parser={(value: any) => value.replace("台", "")}
  278 + min={0}
  279 + max={MAX_NUM}
  280 + style={{ width: "100%" }}
  281 + disabled={isReadOnly}
  282 + precision={0}
  283 + />
  284 + </Form.Item>
  285 + <Form.Item name="seriesTaskCount" label="车系任务台数:" required>
  286 + <InputNumber
  287 + formatter={(value) => `${value}台`}
  288 + parser={(value: any) => value.replace("台", "")}
  289 + min={0}
  290 + max={MAX_NUM}
  291 + style={{ width: "100%" }}
  292 + precision={0}
  293 + disabled
  294 + />
  295 + </Form.Item>
  296 + </Form>
  297 + </div>
  298 + <Divider />
  299 + <div style={{ width: 950, margin: "0 auto" }}>
  300 + <Row
  301 + align="middle"
  302 + justify="space-between"
  303 + style={{ marginBottom: 20 }}
  304 + >
  305 + <h2 className={styles.carTask}>车系任务</h2>
  306 + {!isReadOnly && (
  307 + <Button
  308 + icon={<PlusOutlined />}
  309 + type="primary"
  310 + onClick={() => {
  311 + setSeriesVisible(true);
  312 + }}
  313 + >
  314 + 添加车系
  315 + </Button>
  316 + )}
  317 + </Row>
  318 + <Form form={seriesForm} component={false}>
  319 + <Table
  320 + scroll={{ y: 400 }}
  321 + rowKey="seriesId"
  322 + rowClassName={() => "editable-cell"}
  323 + pagination={false}
  324 + components={components}
  325 + dataSource={[...(editAdviser?.seriesTaskList ?? [])]}
  326 + >
  327 + <Column
  328 + title="车系"
  329 + dataIndex="seriesName"
  330 + render={(value, record: API.SeriesTaskItem) => {
  331 + return (
  332 + <Row align="middle" justify="start">
  333 + <span>{value}</span>
  334 + {record.newEnergy && (
  335 + <Tag
  336 + style={{ marginBottom: 5, marginLeft: 7 }}
  337 + color="green"
  338 + >
  339 + 新能源
  340 + </Tag>
  341 + )}
  342 + </Row>
  343 + );
  344 + }}
  345 + />
  346 + <Column
  347 + title="零售任务(台)"
  348 + dataIndex="taskCount"
  349 + onCell={(record: API.SeriesTaskItem) => ({
  350 + record,
  351 + inputType: "number",
  352 + dataIndex: "taskCount",
  353 + title: "零售任务(台)",
  354 + editing: isEditing(record),
  355 + })}
  356 + />
  357 + {!isReadOnly && (
  358 + <Column
  359 + width={120}
  360 + title="操作"
  361 + render={(text, record: API.SeriesTaskItem) => {
  362 + const editable = isEditing(record);
  363 + return (
  364 + <div>
  365 + {editable ? (
  366 + <span>
  367 + <a onClick={() => setSeriesRow(record.seriesId)}>
  368 + 确认
  369 + </a>
  370 + <Divider type="vertical" />
  371 + <a
  372 + style={{ color: "#999" }}
  373 + onClick={() => setEditingKey(-1)}
  374 + >
  375 + 取消
  376 + </a>
  377 + </span>
  378 + ) : (
  379 + <span>
  380 + <a onClick={() => editSeriesRow(record)}>编辑</a>
  381 + <Divider type="vertical" />
  382 + <a
  383 + style={{ color: "#999" }}
  384 + onClick={() => {
  385 + deleteAdviserSeriesRow(record.seriesId);
  386 + }}
  387 + >
  388 + 删除
  389 + </a>
  390 + </span>
  391 + )}
  392 + </div>
  393 + );
  394 + }}
  395 + />
  396 + )}
  397 + </Table>
  398 + </Form>
  399 + </div>
  400 + <SeriesModal
  401 + visible={seriesVisible}
  402 + selectedIds={selectedIds}
  403 + onOk={handleSelectSeries}
  404 + onCancel={() => {
  405 + setSeriesVisible(false);
  406 + }}
  407 + />
  408 + </div>
  409 + );
  410 +}
  411 +
  412 +export default observer(AdviserTaskEdit, { forwardRef: true });
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/components/EditableCell.tsx 0 → 100644
  1 +import React, { HTMLAttributes } from "react";
  2 +import { Input, InputNumber, Form } from "antd";
  3 +import _ from "lodash";
  4 +import { MAX_NUM } from "../../../entity";
  5 +
  6 +interface EditableCellProps extends HTMLAttributes<HTMLElement> {
  7 + editing: boolean;
  8 + dataIndex: string;
  9 + title: string;
  10 + inputType: "number" | "text";
  11 + record: any;
  12 + index: number;
  13 + children: React.ReactNode;
  14 +}
  15 +
  16 +function EditableCell(props: EditableCellProps) {
  17 + const { editing, dataIndex, title, inputType, record, index, ...restProps } =
  18 + props;
  19 + let timer: any = null;
  20 +
  21 + const check = async (rule: any, value: any): Promise<any> => {
  22 + timer && clearTimeout(timer);
  23 + return new Promise((resolve, reject) => {
  24 + timer = setTimeout(async () => {
  25 + switch (dataIndex) {
  26 + default:
  27 + resolve(true);
  28 + break;
  29 + }
  30 + }, 500);
  31 + });
  32 + };
  33 +
  34 + return (
  35 + <td {...restProps}>
  36 + {editing ? (
  37 + <Form.Item
  38 + name={dataIndex}
  39 + style={{ margin: 0 }}
  40 + rules={[
  41 + {
  42 + required: true,
  43 + message: `请输入正确的${title}值`,
  44 + },
  45 + { validator: check },
  46 + ]}
  47 + >
  48 + {inputType === "number" ? (
  49 + <InputNumber min={0} max={MAX_NUM} />
  50 + ) : (
  51 + <Input />
  52 + )}
  53 + </Form.Item>
  54 + ) : (
  55 + restProps.children
  56 + )}
  57 + </td>
  58 + );
  59 +}
  60 +
  61 +export default EditableCell;
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/components/SeriesModal.tsx 0 → 100644
  1 +import React, { useEffect } from "react";
  2 +import { Modal, Form, Select } from "antd";
  3 +import { useRequest } from "umi";
  4 +import * as API from "../../../api";
  5 +import { useStore } from "../../../store";
  6 +
  7 +export interface SeriesModalProps {
  8 + visible: boolean;
  9 + selectedIds: string[];
  10 + onOk: (values: any) => void;
  11 + onCancel: () => void;
  12 +}
  13 +
  14 +export default function SeriesModal({
  15 + visible,
  16 + selectedIds,
  17 + onOk,
  18 + onCancel,
  19 +}: SeriesModalProps) {
  20 + const { shopTaskItem } = useStore();
  21 + const [form] = Form.useForm();
  22 +
  23 + const getBrandsHook = useRequest(API.getBrands, {
  24 + manual: true,
  25 + });
  26 +
  27 + useEffect(() => {
  28 + getBrandsHook.run({ shopId: shopTaskItem?.shopId! });
  29 + }, []);
  30 +
  31 + const getSeriesHook = useRequest(API.getSeries, {
  32 + manual: true,
  33 + // 车系去重
  34 + formatResult: ({ data }) => {
  35 + const formatData: API.SeriesItem[] = [];
  36 + data?.forEach((item) => {
  37 + if (selectedIds.includes(String(item.id))) return;
  38 + formatData.push(item);
  39 + });
  40 + return formatData;
  41 + },
  42 + });
  43 +
  44 + const onFinish = (values: any) => {
  45 + const newSeries = (values.series ?? []).map((item: any) => {
  46 + const currSeries = (getSeriesHook.data ?? []).filter(
  47 + (i) => i.id === item.value
  48 + );
  49 + return {
  50 + ...item,
  51 + newEnergy: currSeries[0].newEnergy,
  52 + };
  53 + });
  54 + onOk({ ...values, series: newSeries });
  55 + };
  56 +
  57 + return (
  58 + <Modal
  59 + title="选择车系"
  60 + open={visible}
  61 + onCancel={onCancel}
  62 + onOk={() => form.submit()}
  63 + width={600}
  64 + destroyOnClose
  65 + afterClose={() => form.resetFields()}
  66 + >
  67 + <Form
  68 + form={form}
  69 + labelCol={{ span: 6 }}
  70 + wrapperCol={{ span: 15 }}
  71 + onFinish={onFinish}
  72 + >
  73 + <Form.Item
  74 + label="品牌"
  75 + name="brand"
  76 + rules={[{ required: true, message: "请先选择品牌" }]}
  77 + >
  78 + <Select
  79 + showSearch
  80 + allowClear
  81 + labelInValue
  82 + loading={getBrandsHook.loading}
  83 + placeholder="请先选择品牌"
  84 + style={{ width: "100%" }}
  85 + fieldNames={{ value: "id", label: "name" }}
  86 + options={getBrandsHook.data}
  87 + />
  88 + </Form.Item>
  89 + <Form.Item
  90 + noStyle
  91 + shouldUpdate={(prevValues, currentValues) => {
  92 + if (prevValues.brand !== currentValues.brand) {
  93 + form.setFieldValue("series", undefined);
  94 + getSeriesHook.run({
  95 + shopId: shopTaskItem?.shopId!,
  96 + brandId: currentValues.brand.value,
  97 + });
  98 + }
  99 + return prevValues.brand !== currentValues.brand;
  100 + }}
  101 + >
  102 + {({ getFieldValue }) => {
  103 + if (!getFieldValue("brand")) return null;
  104 + return (
  105 + <Form.Item
  106 + label="车系"
  107 + name="series"
  108 + rules={[{ required: true, message: "请选择车系" }]}
  109 + >
  110 + <Select
  111 + mode="multiple"
  112 + showSearch
  113 + allowClear
  114 + labelInValue
  115 + loading={getSeriesHook.loading}
  116 + placeholder="请选择车系"
  117 + style={{ width: "100%" }}
  118 + fieldNames={{ value: "id", label: "name" }}
  119 + options={getSeriesHook.data}
  120 + />
  121 + </Form.Item>
  122 + );
  123 + }}
  124 + </Form.Item>
  125 + </Form>
  126 + </Modal>
  127 + );
  128 +}
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/components/ShopTask.tsx 0 → 100644
  1 +import React, { useEffect, useMemo, useState } from "react";
  2 +import {
  3 + InputNumber,
  4 + Divider,
  5 + Form,
  6 + message,
  7 + Table,
  8 + Button,
  9 + Row,
  10 + Tag,
  11 +} from "antd";
  12 +import { PlusOutlined } from "@ant-design/icons";
  13 +import * as API from "../../../api";
  14 +import styles from "../index.less";
  15 +import { useStore } from "../../../store";
  16 +import _ from "lodash";
  17 +import EditableCell from "./EditableCell";
  18 +import SeriesModal from "./SeriesModal";
  19 +import { history, useRequest } from "umi";
  20 +import { MAX_NUM } from "../../../entity";
  21 +
  22 +const { Column } = Table;
  23 +
  24 +interface ShopTaskProps {
  25 + form: any;
  26 +}
  27 +
  28 +export default function ShopTask({ form }: ShopTaskProps) {
  29 + const {
  30 + shopTaskItem,
  31 + isReadOnly,
  32 + setShopSeriesRow,
  33 + deleteShopSeriesRow,
  34 + addShopSeriesRow,
  35 + } = useStore();
  36 + const [seriesForm] = Form.useForm();
  37 + const [editingKey, setEditingKey] = useState(-1); // 编辑表格 key
  38 + const [seriesVisible, setSeriesVisible] = useState(false);
  39 +
  40 + const saveShopSaleTaskHook = useRequest(API.saveShopSaleTask, {
  41 + manual: true,
  42 + throwOnError: true,
  43 + });
  44 +
  45 + // 车系去重
  46 + const selectedIds = useMemo(() => {
  47 + if (!shopTaskItem) return [];
  48 + return shopTaskItem.seriesTaskList.map((item) => String(item.seriesId));
  49 + }, [shopTaskItem]);
  50 +
  51 + // 计算车系任务总数自动填写
  52 + useEffect(() => {
  53 + const total = shopTaskItem?.seriesTaskList.reduce(
  54 + (total, currItem) => total + currItem.taskCount,
  55 + 0
  56 + );
  57 + form.setFieldValue("seriesTaskCount", total);
  58 + }, [shopTaskItem]);
  59 +
  60 + const isEditing = (record: API.SeriesTaskItem) => {
  61 + return record.seriesId === Number(editingKey);
  62 + };
  63 +
  64 + const setSeriesRow = (id: number) => {
  65 + seriesForm
  66 + .validateFields()
  67 + .then((row: any) => {
  68 + setEditingKey(-1);
  69 + setShopSeriesRow(id, row);
  70 + })
  71 + .catch((error: any) => {
  72 + message.error(error.message ?? "表单校验失败");
  73 + });
  74 + };
  75 +
  76 + const editSeriesRow = (record: API.SeriesTaskItem) => {
  77 + seriesForm.setFieldsValue({ ...record });
  78 + setEditingKey(record.seriesId);
  79 + };
  80 +
  81 + // 添加车系
  82 + const handleSelectSeries = (values: any) => {
  83 + addShopSeriesRow(values);
  84 + setSeriesVisible(false);
  85 + };
  86 +
  87 + const handleGoBack = () => {
  88 + history.goBack(); // todo 提示是否有未保存的修改
  89 + };
  90 +
  91 + const handleSaveTask = async () => {
  92 + await form.validateFields();
  93 + const values = form.getFieldsValue();
  94 + const { taskId, ...other } = shopTaskItem!;
  95 + saveShopSaleTaskHook
  96 + .run({ ...other, ...values, id: taskId })
  97 + .then(() => {
  98 + message.success("保存草稿成功");
  99 + })
  100 + .catch((error: any) => {
  101 + message.error(error.message ?? "请求失败");
  102 + });
  103 + };
  104 +
  105 + const layout = {
  106 + labelCol: { span: 5 },
  107 + wrapperCol: { span: 19 },
  108 + };
  109 +
  110 + const components = {
  111 + body: {
  112 + cell: EditableCell,
  113 + },
  114 + };
  115 +
  116 + return (
  117 + <div style={{ width: 1200, margin: "0 auto" }}>
  118 + <div style={{ width: 880, margin: "0 auto", paddingTop: 24 }}>
  119 + <Form
  120 + {...layout}
  121 + labelAlign="left"
  122 + form={form}
  123 + initialValues={shopTaskItem!}
  124 + >
  125 + <Form.Item name="taskCount" label="零售任务:" required>
  126 + <InputNumber
  127 + formatter={(value) => `${value}台`}
  128 + parser={(value: any) => value.replace("台", "")}
  129 + min={0}
  130 + max={MAX_NUM}
  131 + style={{ width: "100%" }}
  132 + disabled={isReadOnly}
  133 + precision={0}
  134 + />
  135 + </Form.Item>
  136 + <Form.Item
  137 + noStyle
  138 + shouldUpdate={(prevValues, currentValues) => {
  139 + if (prevValues.taskCount !== currentValues.taskCount) {
  140 + form.setFieldValue(
  141 + "clueDealTaskCount",
  142 + (
  143 + currentValues.taskCount *
  144 + ((shopTaskItem?.clueDealTaskRate ?? 100) / 100)
  145 + ).toFixed(2)
  146 + );
  147 + }
  148 + return prevValues.taskCount !== currentValues.taskCount;
  149 + }}
  150 + >
  151 + {() => {
  152 + return (
  153 + <Form.Item
  154 + name="clueDealTaskCount"
  155 + label="线索到店零售台数:"
  156 + required
  157 + >
  158 + <InputNumber
  159 + formatter={(value) => `${value}台`}
  160 + parser={(value: any) => value.replace("台", "")}
  161 + min={0}
  162 + max={MAX_NUM}
  163 + style={{ width: "100%" }}
  164 + disabled={isReadOnly}
  165 + precision={2}
  166 + />
  167 + </Form.Item>
  168 + );
  169 + }}
  170 + </Form.Item>
  171 + <Form.Item
  172 + name="newEnergyTaskCount"
  173 + label="新能源车任务台数:"
  174 + required
  175 + dependencies={["taskCount", "fuelVehicleTaskCount"]}
  176 + rules={[
  177 + ({ getFieldValue }) => ({
  178 + validator(_, value) {
  179 + const taskCount = getFieldValue("taskCount");
  180 + const fuelVehicleTaskCount = getFieldValue(
  181 + "fuelVehicleTaskCount"
  182 + );
  183 + if (
  184 + value >= 0 &&
  185 + fuelVehicleTaskCount + value === taskCount
  186 + ) {
  187 + return Promise.resolve();
  188 + }
  189 + return Promise.reject(
  190 + new Error(
  191 + "规则:新能源车任务台数 + 传统燃油车任务台数 = 零售任务台数"
  192 + )
  193 + );
  194 + },
  195 + }),
  196 + ]}
  197 + >
  198 + <InputNumber
  199 + formatter={(value) => `${value}台`}
  200 + parser={(value: any) => value.replace("台", "")}
  201 + min={0}
  202 + max={MAX_NUM}
  203 + style={{ width: "100%" }}
  204 + disabled={isReadOnly}
  205 + precision={0}
  206 + />
  207 + </Form.Item>
  208 +
  209 + <Form.Item
  210 + name="fuelVehicleTaskCount"
  211 + label="传统燃油车任务台数:"
  212 + required
  213 + dependencies={["taskCount", "newEnergyTaskCount"]}
  214 + rules={[
  215 + ({ getFieldValue }) => ({
  216 + validator(_, value) {
  217 + const taskCount = getFieldValue("taskCount");
  218 + const newEnergyTaskCount =
  219 + getFieldValue("newEnergyTaskCount");
  220 + if (value >= 0 && newEnergyTaskCount + value === taskCount) {
  221 + return Promise.resolve();
  222 + }
  223 + return Promise.reject(
  224 + new Error(
  225 + "规则:新能源车任务台数 + 传统燃油车任务台数 = 零售任务台数"
  226 + )
  227 + );
  228 + },
  229 + }),
  230 + ]}
  231 + >
  232 + <InputNumber
  233 + formatter={(value) => `${value}台`}
  234 + parser={(value: any) => value.replace("台", "")}
  235 + min={0}
  236 + max={MAX_NUM}
  237 + style={{ width: "100%" }}
  238 + disabled={isReadOnly}
  239 + precision={0}
  240 + />
  241 + </Form.Item>
  242 + <Form.Item
  243 + name="vehicleGrossProfitTask"
  244 + label="车辆毛利任务:"
  245 + required
  246 + >
  247 + <InputNumber
  248 + formatter={(value) => `${value}元`}
  249 + parser={(value: any) => value.replace("元", "")}
  250 + min={0}
  251 + max={MAX_NUM}
  252 + style={{ width: "100%" }}
  253 + disabled={isReadOnly}
  254 + precision={2}
  255 + />
  256 + </Form.Item>
  257 + <Form.Item
  258 + name="testDriveTaskCount"
  259 + label="首客试驾成交任务台数:"
  260 + required
  261 + >
  262 + <InputNumber
  263 + formatter={(value) => `${value}台`}
  264 + parser={(value: any) => value.replace("台", "")}
  265 + min={0}
  266 + max={MAX_NUM}
  267 + style={{ width: "100%" }}
  268 + disabled={isReadOnly}
  269 + precision={0}
  270 + />
  271 + </Form.Item>
  272 + <Form.Item name="tackCarTaskCount" label="攻坚车任务台数:" required>
  273 + <InputNumber
  274 + formatter={(value) => `${value}台`}
  275 + parser={(value: any) => value.replace("台", "")}
  276 + min={0}
  277 + max={MAX_NUM}
  278 + style={{ width: "100%" }}
  279 + disabled={isReadOnly}
  280 + precision={0}
  281 + />
  282 + </Form.Item>
  283 + <Form.Item name="seriesTaskCount" label="车系任务台数:" required>
  284 + <InputNumber
  285 + formatter={(value) => `${value}台`}
  286 + parser={(value: any) => value.replace("台", "")}
  287 + min={0}
  288 + max={MAX_NUM}
  289 + style={{ width: "100%" }}
  290 + precision={0}
  291 + disabled
  292 + />
  293 + </Form.Item>
  294 + </Form>
  295 + </div>
  296 + <Divider />
  297 + <div style={{ width: 880, margin: "0 auto" }}>
  298 + <Row
  299 + align="middle"
  300 + justify="space-between"
  301 + style={{ marginBottom: 20 }}
  302 + >
  303 + <h2 className={styles.carTask}>车系任务</h2>
  304 + {!isReadOnly && (
  305 + <Button
  306 + icon={<PlusOutlined />}
  307 + type="primary"
  308 + onClick={() => {
  309 + setSeriesVisible(true);
  310 + }}
  311 + >
  312 + 添加车系
  313 + </Button>
  314 + )}
  315 + </Row>
  316 + <Form form={seriesForm} component={false}>
  317 + <Table
  318 + scroll={{ y: 400 }}
  319 + rowKey="seriesId"
  320 + rowClassName={() => "editable-cell"}
  321 + pagination={false}
  322 + components={components}
  323 + dataSource={[...shopTaskItem?.seriesTaskList!]}
  324 + >
  325 + <Column
  326 + title="车系"
  327 + dataIndex="seriesName"
  328 + render={(value, record: API.SeriesTaskItem) => {
  329 + return (
  330 + <Row align="middle" justify="start">
  331 + <span>{value}</span>
  332 + {record.newEnergy && (
  333 + <Tag
  334 + style={{ marginBottom: 5, marginLeft: 7 }}
  335 + color="green"
  336 + >
  337 + 新能源
  338 + </Tag>
  339 + )}
  340 + </Row>
  341 + );
  342 + }}
  343 + />
  344 + <Column
  345 + title="零售任务(台)"
  346 + dataIndex="taskCount"
  347 + onCell={(record: API.SeriesTaskItem) => ({
  348 + record,
  349 + inputType: "number",
  350 + dataIndex: "taskCount",
  351 + title: "零售任务(台)",
  352 + editing: isEditing(record),
  353 + })}
  354 + />
  355 + {!isReadOnly && (
  356 + <Column
  357 + width={120}
  358 + title="操作"
  359 + render={(text, record: API.SeriesTaskItem) => {
  360 + const editable = isEditing(record);
  361 + return (
  362 + <div>
  363 + {editable ? (
  364 + <span>
  365 + <a onClick={() => setSeriesRow(record.seriesId)}>
  366 + 确认
  367 + </a>
  368 + <Divider type="vertical" />
  369 + <a
  370 + style={{ color: "#999" }}
  371 + onClick={() => setEditingKey(-1)}
  372 + >
  373 + 取消
  374 + </a>
  375 + </span>
  376 + ) : (
  377 + <span>
  378 + <a onClick={() => editSeriesRow(record)}>编辑</a>
  379 + <Divider type="vertical" />
  380 + <a
  381 + style={{ color: "#999" }}
  382 + onClick={() => deleteShopSeriesRow(record.seriesId)}
  383 + >
  384 + 删除
  385 + </a>
  386 + </span>
  387 + )}
  388 + </div>
  389 + );
  390 + }}
  391 + />
  392 + )}
  393 + </Table>
  394 + </Form>
  395 + </div>
  396 + <Row align="middle" justify="center" style={{ marginTop: 50 }}>
  397 + <Button onClick={handleGoBack}>返回列表</Button>
  398 + {!isReadOnly && (
  399 + <Button
  400 + type="primary"
  401 + style={{ marginLeft: 10 }}
  402 + loading={saveShopSaleTaskHook.loading}
  403 + onClick={handleSaveTask}
  404 + >
  405 + 保存草稿
  406 + </Button>
  407 + )}
  408 + </Row>
  409 + <SeriesModal
  410 + visible={seriesVisible}
  411 + selectedIds={selectedIds}
  412 + onOk={handleSelectSeries}
  413 + onCancel={() => {
  414 + setSeriesVisible(false);
  415 + }}
  416 + />
  417 + </div>
  418 + );
  419 +}
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/index.less 0 → 100644
  1 +.carTask {
  2 + font-size: 18px;
  3 + color: #333333;
  4 + font-weight: 500;
  5 + margin-bottom: 0;
  6 +}
0 7 \ No newline at end of file
... ...
src/pages/order3/SaleTask/subpages/TaskEdit/index.tsx 0 → 100644
  1 +import React, { useEffect, useState } from "react";
  2 +import { Button, Card, Form, Row, Tabs } from "antd";
  3 +import { PageHeaderWrapper } from "@ant-design/pro-layout";
  4 +import { history } from "umi";
  5 +import { Provider, useStore } from "../../store";
  6 +import ShopTask from "./components/ShopTask";
  7 +import AdviserTask from "./components/AdviserTask";
  8 +import useInitial from "@/hooks/useInitail";
  9 +import * as API from "../../api";
  10 +import { isEmpty } from "lodash";
  11 +
  12 +export default () => (
  13 + <Provider>
  14 + <TaskEdit />
  15 + </Provider>
  16 +);
  17 +
  18 +function TaskEdit() {
  19 + const querys: any = history.location?.query;
  20 + const readOnly = querys?.readOnly === "1";
  21 + const shopId = querys?.shopId;
  22 + const taskDate = querys?.taskDate;
  23 + const { shopTaskItem, setShopTaskItem, setIsReadOnly } = useStore();
  24 + const [currStep, setCurrStep] = useState("1");
  25 + const [shopTaskForm] = Form.useForm();
  26 +
  27 + // 获取门店零售任务详情
  28 + const { data } = useInitial<API.ShopTaskItem, API.GetShopSaleTaskReq>(
  29 + API.getShopSaleTask,
  30 + {} as API.ShopTaskItem,
  31 + {
  32 + shopId,
  33 + taskDate,
  34 + }
  35 + );
  36 +
  37 + useEffect(() => {
  38 + setShopTaskItem(data);
  39 + setIsReadOnly(readOnly);
  40 + }, [data, readOnly]);
  41 +
  42 + return (
  43 + <PageHeaderWrapper title={false}>
  44 + <Card
  45 + bodyStyle={{ paddingTop: 0 }}
  46 + title={<span>当前选择门店:{shopTaskItem?.shopName}</span>}
  47 + >
  48 + <Tabs
  49 + defaultActiveKey={currStep}
  50 + onChange={(activeKey) => setCurrStep(activeKey)}
  51 + items={[
  52 + {
  53 + label: `门店任务分配${readOnly ? "详情" : ""}`,
  54 + key: "1",
  55 + children: !isEmpty(shopTaskItem) && (
  56 + <ShopTask form={shopTaskForm} />
  57 + ),
  58 + },
  59 + {
  60 + label: `销售顾问任务分配${readOnly ? "详情" : ""}`,
  61 + key: "2",
  62 + children: !isEmpty(shopTaskItem) && (
  63 + <AdviserTask form={shopTaskForm} />
  64 + ),
  65 + },
  66 + ]}
  67 + />
  68 + {isEmpty(shopTaskItem) && (
  69 + <Row align="middle" justify="center" style={{ marginTop: 50 }}>
  70 + <Button onClick={() => history.goBack()}>返回列表</Button>
  71 + </Row>
  72 + )}
  73 + </Card>
  74 + </PageHeaderWrapper>
  75 + );
  76 +}
... ...
src/utils/host.ts
... ... @@ -39,6 +39,7 @@ export const PROENGINE_HOST = &#39;/proengine&#39;;
39 39 export const MCAR_HOST = '/mcar';
40 40 /** 订单系统 */
41 41 export const ORDER_HOST = '/order';
  42 +export const ORDER3_HOST = '/order3';
42 43 /** 卡系统 */
43 44 export const CARD_HOST = '/card';
44 45 /** 潘多拉系统 */
... ...