From 6f1a96c44b273f3dee28260715c16d408d79d38f Mon Sep 17 00:00:00 2001 From: luxiaotao1123 <t1341870251@163.com> Date: 星期五, 27 九月 2024 10:47:36 +0800 Subject: [PATCH] # --- zy-acs-flow/src/page/mission/MissionColumn.jsx | 75 ++++++++++ zy-acs-flow/src/page/mission/MissionListContent.jsx | 242 ++++++++++++++++++++++++++++++++++ zy-acs-flow/src/page/mission/MissionCard.jsx | 78 +++++++++++ zy-acs-flow/src/page/mission/MissionEmpty.jsx | 6 zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/MissionServiceImpl.java | 2 5 files changed, 399 insertions(+), 4 deletions(-) diff --git a/zy-acs-flow/src/page/mission/MissionCard.jsx b/zy-acs-flow/src/page/mission/MissionCard.jsx new file mode 100644 index 0000000..961aac4 --- /dev/null +++ b/zy-acs-flow/src/page/mission/MissionCard.jsx @@ -0,0 +1,78 @@ +import { Draggable } from '@hello-pangea/dnd'; +import { Box, Card, Typography } from '@mui/material'; +import { ReferenceField, useRedirect } from 'react-admin'; +import { CompanyAvatar } from '../companies/CompanyAvatar'; +import { Deal } from '../types'; + +export const MissionCard = ({ deal, index }) => { + if (!deal) return null; + + return ( + <Draggable draggableId={String(deal.id)} index={index}> + {(provided, snapshot) => ( + <DealCardContent + provided={provided} + snapshot={snapshot} + deal={deal} + /> + )} + </Draggable> + ); +}; + +export const DealCardContent = ({ + provided, + snapshot, + deal, +}) => { + const redirect = useRedirect(); + const handleClick = () => { + redirect(`/deals/${deal.id}/show`, undefined, undefined, undefined, { + _scrollToTop: false, + }); + }; + + return ( + <Box + sx={{ marginBottom: 1, cursor: 'pointer' }} + {...provided?.draggableProps} + {...provided?.dragHandleProps} + ref={provided?.innerRef} + onClick={handleClick} + > + <Card + style={{ + opacity: snapshot?.isDragging ? 0.9 : 1, + transform: snapshot?.isDragging ? 'rotate(-2deg)' : '', + }} + elevation={snapshot?.isDragging ? 3 : 1} + > + <Box padding={1} display="flex"> + <ReferenceField + source="company_id" + record={deal} + reference="companies" + link={false} + > + <CompanyAvatar width={20} height={20} /> + </ReferenceField> + <Box sx={{ marginLeft: 1 }}> + <Typography variant="body2" gutterBottom> + {deal.name} + </Typography> + <Typography variant="caption" color="textSecondary"> + {deal.amount.toLocaleString('en-US', { + notation: 'compact', + style: 'currency', + currency: 'USD', + currencyDisplay: 'narrowSymbol', + minimumSignificantDigits: 3, + })} + {deal.category ? `, ${deal.category}` : ''} + </Typography> + </Box> + </Box> + </Card> + </Box> + ); +}; diff --git a/zy-acs-flow/src/page/mission/MissionColumn.jsx b/zy-acs-flow/src/page/mission/MissionColumn.jsx new file mode 100644 index 0000000..bdb5aad --- /dev/null +++ b/zy-acs-flow/src/page/mission/MissionColumn.jsx @@ -0,0 +1,75 @@ +import { Droppable } from '@hello-pangea/dnd'; +import { Box, Stack, Typography } from '@mui/material'; + +import { Deal } from '../types'; +import { DealCard } from './MissionCard'; +import { useConfigurationContext } from '../root/ConfigurationContext'; +import { findDealLabel } from './deal'; + +export const MissionColumn = ({ stage, deals, }) => { + const totalAmount = deals.reduce((sum, deal) => sum + deal.amount, 0); + + const { dealStages } = useConfigurationContext(); + return ( + <Box + sx={{ + flex: 1, + paddingTop: '8px', + paddingBottom: '16px', + bgcolor: '#eaeaee', + '&:first-of-type': { + paddingLeft: '5px', + borderTopLeftRadius: 5, + }, + '&:last-of-type': { + paddingRight: '5px', + borderTopRightRadius: 5, + }, + }} + > + <Stack alignItems="center"> + <Typography variant="subtitle1"> + {findDealLabel(dealStages, stage)} + </Typography> + <Typography + variant="subtitle1" + color="text.secondary" + fontSize="small" + > + {totalAmount.toLocaleString('en-US', { + notation: 'compact', + style: 'currency', + currency: 'USD', + currencyDisplay: 'narrowSymbol', + minimumSignificantDigits: 3, + })} + </Typography> + </Stack> + <Droppable droppableId={stage}> + {(droppableProvided, snapshot) => ( + <Box + ref={droppableProvided.innerRef} + {...droppableProvided.droppableProps} + className={ + snapshot.isDraggingOver ? ' isDraggingOver' : '' + } + sx={{ + display: 'flex', + flexDirection: 'column', + borderRadius: 1, + padding: '5px', + '&.isDraggingOver': { + bgcolor: '#dadadf', + }, + }} + > + {deals.map((deal, index) => ( + <DealCard key={deal.id} deal={deal} index={index} /> + ))} + {droppableProvided.placeholder} + </Box> + )} + </Droppable> + </Box> + ); +}; diff --git a/zy-acs-flow/src/page/mission/MissionEmpty.jsx b/zy-acs-flow/src/page/mission/MissionEmpty.jsx index 84d66eb..222ca88 100644 --- a/zy-acs-flow/src/page/mission/MissionEmpty.jsx +++ b/zy-acs-flow/src/page/mission/MissionEmpty.jsx @@ -8,11 +8,11 @@ export const MissionEmpty = ({ children }) => { const location = useLocation(); - const matchCreate = matchPath('/deals/create', location.pathname); - const appbarHeight = useAppBarHeight(); + const matchCreate = matchPath('/mission/create', location.pathname); + const appBarHeight = useAppBarHeight(); // get Contact data - const { data: contacts, isPending: contactsLoading } = useGetList<Contact>( + const { data: contacts, isPending: contactsLoading } = useGetList < Contact > ( 'contacts', { pagination: { page: 1, perPage: 1 }, diff --git a/zy-acs-flow/src/page/mission/MissionListContent.jsx b/zy-acs-flow/src/page/mission/MissionListContent.jsx new file mode 100644 index 0000000..fad9b6c --- /dev/null +++ b/zy-acs-flow/src/page/mission/MissionListContent.jsx @@ -0,0 +1,242 @@ +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'; + +export const MissionListContent = () => { + const { dealStages } = useConfigurationContext(); + const { data: unorderedDeals, isPending, refetch } = useListContext(); + const dataProvider = useDataProvider(); + + const [dealsByStage, setDealsByStage] = useState( + getDealsByStage([], dealStages) + ); + + useEffect(() => { + if (unorderedDeals) { + const newDealsByStage = getDealsByStage(unorderedDeals, dealStages); + if (!isEqual(newDealsByStage, dealsByStage)) { + setDealsByStage(newDealsByStage); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [unorderedDeals]); + + if (isPending) return null; + + const onDragEnd = result => { + const { destination, source } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + 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} + /> + ))} + </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, + }), + ]); + } +}; diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/MissionServiceImpl.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/MissionServiceImpl.java index 42aea05..7a0f298 100644 --- a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/MissionServiceImpl.java +++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/service/impl/MissionServiceImpl.java @@ -27,7 +27,7 @@ MissionVo vo = new MissionVo(); vo.setGroupNo(groupNo); - + result.add(vo); } return result; -- Gitblit v1.9.1