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