|  |  |  | 
|---|
|  |  |  | import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; | 
|---|
|  |  |  | import { Box } from '@mui/material'; | 
|---|
|  |  |  | import isEqual from 'lodash/isEqual'; | 
|---|
|  |  |  | import { useEffect, useState } from 'react'; | 
|---|
|  |  |  | import { DataProvider, useDataProvider, useListContext } from 'react-admin'; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | import { Deal } from '../types'; | 
|---|
|  |  |  | import { DealColumn } from './MissionColumn'; | 
|---|
|  |  |  | import { DealsByStage, getDealsByStage } from './stages'; | 
|---|
|  |  |  | import { useConfigurationContext } from '../root/ConfigurationContext'; | 
|---|
|  |  |  | import { DragDropContext } from '@hello-pangea/dnd'; | 
|---|
|  |  |  | import { Box, LinearProgress } from '@mui/material'; | 
|---|
|  |  |  | import { | 
|---|
|  |  |  | useDataProvider, | 
|---|
|  |  |  | useListContext, | 
|---|
|  |  |  | useNotify, | 
|---|
|  |  |  | useRefresh, | 
|---|
|  |  |  | useTranslate, | 
|---|
|  |  |  | } from 'react-admin'; | 
|---|
|  |  |  | import { MissionColumn } from './MissionColumn'; | 
|---|
|  |  |  | import request from '@/utils/request'; | 
|---|
|  |  |  | import { CUSTOM_PAGES_DATA_INTERVAL } from '@/config/setting'; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | export const MissionListContent = () => { | 
|---|
|  |  |  | const { dealStages } = useConfigurationContext(); | 
|---|
|  |  |  | const { data: unorderedDeals, isPending, refetch } = useListContext(); | 
|---|
|  |  |  | const translate = useTranslate(); | 
|---|
|  |  |  | const notify = useNotify(); | 
|---|
|  |  |  | const refresh = useRefresh(); | 
|---|
|  |  |  | const dataProvider = useDataProvider(); | 
|---|
|  |  |  | const { data, isPending, refetch } = useListContext(); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | const [dealsByStage, setDealsByStage] = useState( | 
|---|
|  |  |  | getDealsByStage([], dealStages) | 
|---|
|  |  |  | ); | 
|---|
|  |  |  | const [stages, setStages] = useState([]); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | useEffect(() => { | 
|---|
|  |  |  | if (unorderedDeals) { | 
|---|
|  |  |  | const newDealsByStage = getDealsByStage(unorderedDeals, dealStages); | 
|---|
|  |  |  | if (!isEqual(newDealsByStage, dealsByStage)) { | 
|---|
|  |  |  | setDealsByStage(newDealsByStage); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | const httpStages = async () => { | 
|---|
|  |  |  | request.post('/mission/posType/list').then(res => { | 
|---|
|  |  |  | const { code, msg, data } = res.data; | 
|---|
|  |  |  | if (code === 200) { | 
|---|
|  |  |  | setStages(data); | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | notify(msg || 'common.response.fail', { type: 'error', messageArgs: { _: msg } }); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | }).catch(error => { | 
|---|
|  |  |  | notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } }); | 
|---|
|  |  |  | }) | 
|---|
|  |  |  | } | 
|---|
|  |  |  | // eslint-disable-next-line react-hooks/exhaustive-deps | 
|---|
|  |  |  | }, [unorderedDeals]); | 
|---|
|  |  |  | httpStages(); | 
|---|
|  |  |  | }, [notify]); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | if (isPending) return null; | 
|---|
|  |  |  | useEffect(() => { | 
|---|
|  |  |  | const intervalId = setInterval(() => { | 
|---|
|  |  |  | refetch(); | 
|---|
|  |  |  | }, CUSTOM_PAGES_DATA_INTERVAL); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | return () => clearInterval(intervalId); | 
|---|
|  |  |  | }, [refetch]) | 
|---|
|  |  |  |  | 
|---|
|  |  |  | if (isPending) return <LinearProgress />; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | const onDragEnd = result => { | 
|---|
|  |  |  | const { destination, source } = result; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | if (!destination) { | 
|---|
|  |  |  | return; | 
|---|
|  |  |  | } | 
|---|
|  |  |  | const { droppableId: sourceStage, index: sourceIdx } = source; | 
|---|
|  |  |  | const { droppableId: destinationStage, index: destinationIdx } = destination; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | if ( | 
|---|
|  |  |  | destination.droppableId === source.droppableId && | 
|---|
|  |  |  | destination.index === source.index | 
|---|
|  |  |  | ) { | 
|---|
|  |  |  | if (destinationStage === sourceStage | 
|---|
|  |  |  | && destinationIdx === sourceIdx) { | 
|---|
|  |  |  | return; | 
|---|
|  |  |  | } | 
|---|
|  |  |  |  | 
|---|
|  |  |  | const sourceStage = source.droppableId; | 
|---|
|  |  |  | const destinationStage = destination.droppableId; | 
|---|
|  |  |  | const sourceDeal = dealsByStage[sourceStage][source.index]; | 
|---|
|  |  |  | const destinationDeal = dealsByStage[destinationStage][ | 
|---|
|  |  |  | destination.index | 
|---|
|  |  |  | ] ?? { | 
|---|
|  |  |  | stage: destinationStage, | 
|---|
|  |  |  | index: undefined, // undefined if dropped after the last item | 
|---|
|  |  |  | }; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // compute local state change synchronously | 
|---|
|  |  |  | setDealsByStage( | 
|---|
|  |  |  | updateDealStageLocal( | 
|---|
|  |  |  | sourceDeal, | 
|---|
|  |  |  | { stage: sourceStage, index: source.index }, | 
|---|
|  |  |  | { stage: destinationStage, index: destination.index }, | 
|---|
|  |  |  | dealsByStage | 
|---|
|  |  |  | ) | 
|---|
|  |  |  | ); | 
|---|
|  |  |  |  | 
|---|
|  |  |  | // persist the changes | 
|---|
|  |  |  | updateDealStage(sourceDeal, destinationDeal, dataProvider).then(() => { | 
|---|
|  |  |  | refetch(); | 
|---|
|  |  |  | }); | 
|---|
|  |  |  | }; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | return ( | 
|---|
|  |  |  | <DragDropContext onDragEnd={onDragEnd}> | 
|---|
|  |  |  | <Box display="flex"> | 
|---|
|  |  |  | {dealStages.map(stage => ( | 
|---|
|  |  |  | <DealColumn | 
|---|
|  |  |  | stage={stage.value} | 
|---|
|  |  |  | deals={dealsByStage[stage.value]} | 
|---|
|  |  |  | key={stage.value} | 
|---|
|  |  |  | {stages.map(stage => ( | 
|---|
|  |  |  | <MissionColumn | 
|---|
|  |  |  | key={stage} | 
|---|
|  |  |  | stage={stage} | 
|---|
|  |  |  | missions={data.filter(item => item.posType === stage)} | 
|---|
|  |  |  | /> | 
|---|
|  |  |  | ))} | 
|---|
|  |  |  | </Box> | 
|---|
|  |  |  | </DragDropContext> | 
|---|
|  |  |  | ); | 
|---|
|  |  |  | }; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | const updateDealStageLocal = ( | 
|---|
|  |  |  | sourceDeal, | 
|---|
|  |  |  | source, | 
|---|
|  |  |  | destination, | 
|---|
|  |  |  | dealsByStage | 
|---|
|  |  |  | ) => { | 
|---|
|  |  |  | if (source.stage === destination.stage) { | 
|---|
|  |  |  | // moving deal inside the same column | 
|---|
|  |  |  | const column = dealsByStage[source.stage]; | 
|---|
|  |  |  | column.splice(source.index, 1); | 
|---|
|  |  |  | column.splice(destination.index ?? column.length + 1, 0, sourceDeal); | 
|---|
|  |  |  | return { | 
|---|
|  |  |  | ...dealsByStage, | 
|---|
|  |  |  | [destination.stage]: column, | 
|---|
|  |  |  | }; | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | // moving deal across columns | 
|---|
|  |  |  | const sourceColumn = dealsByStage[source.stage]; | 
|---|
|  |  |  | const destinationColumn = dealsByStage[destination.stage]; | 
|---|
|  |  |  | sourceColumn.splice(source.index, 1); | 
|---|
|  |  |  | destinationColumn.splice( | 
|---|
|  |  |  | destination.index ?? destinationColumn.length + 1, | 
|---|
|  |  |  | 0, | 
|---|
|  |  |  | sourceDeal | 
|---|
|  |  |  | ); | 
|---|
|  |  |  | return { | 
|---|
|  |  |  | ...dealsByStage, | 
|---|
|  |  |  | [source.stage]: sourceColumn, | 
|---|
|  |  |  | [destination.stage]: destinationColumn, | 
|---|
|  |  |  | }; | 
|---|
|  |  |  | } | 
|---|
|  |  |  | }; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | const updateDealStage = async ( | 
|---|
|  |  |  | source, | 
|---|
|  |  |  | destination, | 
|---|
|  |  |  | dataProvider | 
|---|
|  |  |  | ) => { | 
|---|
|  |  |  | if (source.stage === destination.stage) { | 
|---|
|  |  |  | // moving deal inside the same column | 
|---|
|  |  |  | // Fetch all the deals in this stage (because the list may be filtered, but we need to update even non-filtered deals) | 
|---|
|  |  |  | const { data: columnDeals } = await dataProvider.getList('deals', { | 
|---|
|  |  |  | sort: { field: 'index', order: 'ASC' }, | 
|---|
|  |  |  | pagination: { page: 1, perPage: 100 }, | 
|---|
|  |  |  | filter: { stage: source.stage }, | 
|---|
|  |  |  | }); | 
|---|
|  |  |  | const destinationIndex = destination.index ?? columnDeals.length + 1; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | if (source.index > destinationIndex) { | 
|---|
|  |  |  | // deal moved up, eg | 
|---|
|  |  |  | // dest   src | 
|---|
|  |  |  | //  <------ | 
|---|
|  |  |  | // [4, 7, 23, 5] | 
|---|
|  |  |  | await Promise.all([ | 
|---|
|  |  |  | // for all deals between destinationIndex and source.index, increase the index | 
|---|
|  |  |  | ...columnDeals | 
|---|
|  |  |  | .filter( | 
|---|
|  |  |  | deal => | 
|---|
|  |  |  | deal.index >= destinationIndex && | 
|---|
|  |  |  | deal.index < source.index | 
|---|
|  |  |  | ) | 
|---|
|  |  |  | .map(deal => | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: deal.id, | 
|---|
|  |  |  | data: { index: deal.index + 1 }, | 
|---|
|  |  |  | previousData: deal, | 
|---|
|  |  |  | }) | 
|---|
|  |  |  | ), | 
|---|
|  |  |  | // for the deal that was moved, update its index | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: source.id, | 
|---|
|  |  |  | data: { index: destinationIndex }, | 
|---|
|  |  |  | previousData: source, | 
|---|
|  |  |  | }), | 
|---|
|  |  |  | ]); | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | // deal moved down, e.g | 
|---|
|  |  |  | // src   dest | 
|---|
|  |  |  | //  ------> | 
|---|
|  |  |  | // [4, 7, 23, 5] | 
|---|
|  |  |  | await Promise.all([ | 
|---|
|  |  |  | // for all deals between source.index and destinationIndex, decrease the index | 
|---|
|  |  |  | ...columnDeals | 
|---|
|  |  |  | .filter( | 
|---|
|  |  |  | deal => | 
|---|
|  |  |  | deal.index <= destinationIndex && | 
|---|
|  |  |  | deal.index > source.index | 
|---|
|  |  |  | ) | 
|---|
|  |  |  | .map(deal => | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: deal.id, | 
|---|
|  |  |  | data: { index: deal.index - 1 }, | 
|---|
|  |  |  | previousData: deal, | 
|---|
|  |  |  | }) | 
|---|
|  |  |  | ), | 
|---|
|  |  |  | // for the deal that was moved, update its index | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: source.id, | 
|---|
|  |  |  | data: { index: destinationIndex }, | 
|---|
|  |  |  | previousData: source, | 
|---|
|  |  |  | }), | 
|---|
|  |  |  | ]); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | } else { | 
|---|
|  |  |  | // moving deal across columns | 
|---|
|  |  |  | // Fetch all the deals in both stages (because the list may be filtered, but we need to update even non-filtered deals) | 
|---|
|  |  |  | const [{ data: sourceDeals }, { data: destinationDeals }] = | 
|---|
|  |  |  | await Promise.all([ | 
|---|
|  |  |  | dataProvider.getList('deals', { | 
|---|
|  |  |  | sort: { field: 'index', order: 'ASC' }, | 
|---|
|  |  |  | pagination: { page: 1, perPage: 100 }, | 
|---|
|  |  |  | filter: { stage: source.stage }, | 
|---|
|  |  |  | }), | 
|---|
|  |  |  | dataProvider.getList('deals', { | 
|---|
|  |  |  | sort: { field: 'index', order: 'ASC' }, | 
|---|
|  |  |  | pagination: { page: 1, perPage: 100 }, | 
|---|
|  |  |  | filter: { stage: destination.stage }, | 
|---|
|  |  |  | }), | 
|---|
|  |  |  | ]); | 
|---|
|  |  |  | const destinationIndex = | 
|---|
|  |  |  | destination.index ?? destinationDeals.length + 1; | 
|---|
|  |  |  |  | 
|---|
|  |  |  | await Promise.all([ | 
|---|
|  |  |  | // decrease index on the deals after the source index in the source columns | 
|---|
|  |  |  | ...sourceDeals | 
|---|
|  |  |  | .filter(deal => deal.index > source.index) | 
|---|
|  |  |  | .map(deal => | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: deal.id, | 
|---|
|  |  |  | data: { index: deal.index - 1 }, | 
|---|
|  |  |  | previousData: deal, | 
|---|
|  |  |  | }) | 
|---|
|  |  |  | ), | 
|---|
|  |  |  | // increase index on the deals after the destination index in the destination columns | 
|---|
|  |  |  | ...destinationDeals | 
|---|
|  |  |  | .filter(deal => deal.index >= destinationIndex) | 
|---|
|  |  |  | .map(deal => | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: deal.id, | 
|---|
|  |  |  | data: { index: deal.index + 1 }, | 
|---|
|  |  |  | previousData: deal, | 
|---|
|  |  |  | }) | 
|---|
|  |  |  | ), | 
|---|
|  |  |  | // change the dragged deal to take the destination index and column | 
|---|
|  |  |  | dataProvider.update('deals', { | 
|---|
|  |  |  | id: source.id, | 
|---|
|  |  |  | data: { | 
|---|
|  |  |  | index: destinationIndex, | 
|---|
|  |  |  | stage: destination.stage, | 
|---|
|  |  |  | }, | 
|---|
|  |  |  | previousData: source, | 
|---|
|  |  |  | }), | 
|---|
|  |  |  | ]); | 
|---|
|  |  |  | } | 
|---|
|  |  |  | }; | 
|---|