Skip to content

Commit

Permalink
Merge pull request #1038 from boxwise/feature/boxview-history-overaly
Browse files Browse the repository at this point in the history
BoxView: Add History Overlay
  • Loading branch information
vahidbazzaz authored Oct 19, 2023
2 parents 6c98358 + 1fcfd62 commit e3f64f0
Show file tree
Hide file tree
Showing 21 changed files with 400 additions and 184 deletions.
40 changes: 40 additions & 0 deletions react/src/components/HistoryOverlay/HistoryOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from "@chakra-ui/react";
import Timeline, { IGroupedRecordEntry } from "../Timeline/Timeline";

interface IHistoryOverlay {
data: IGroupedRecordEntry[];
isOpen: boolean;
onClose: () => void;
}

function HistoryOverlay({ data, isOpen, onClose }: IHistoryOverlay) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
blockScrollOnMount={false}
size="3xl"
scrollBehavior="inside"
>
<ModalOverlay />
<ModalContent borderRadius="0">
<ModalHeader>Box History</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Timeline records={data as IGroupedRecordEntry[]} />
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
);
}

export default HistoryOverlay;
2 changes: 1 addition & 1 deletion react/src/components/Table/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function SelectColumnFilter({
({
label,
value: optionValues[label],
} as ISelectOption),
}) as ISelectOption,
);
}, [id, preFilteredRows]);

Expand Down
55 changes: 55 additions & 0 deletions react/src/components/Timeline/Timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Box, Flex, Text } from "@chakra-ui/react";
import { User } from "types/generated/graphql";
import TimelineEntry from "./components/TimelineEntry";

export interface ITimelineEntry {
action: string;
createdOn: Date;
createdBy: User;
}

export interface IGroupedRecordEntry {
date: string;
entries: (ITimelineEntry | null | undefined)[];
}

export interface ITimelineProps {
records: IGroupedRecordEntry[];
}

function Timeline({ records }: ITimelineProps) {
return (
<Box position="relative">
{records.map(({ date, entries }, index) => (
<Box key={date}>
<Flex
border={1}
borderColor="red.500"
background="red.500"
padding={1}
alignContent="center"
alignItems="center"
justifyContent="center"
maxWidth={120}
rounded={4}
>
<Text fontWeight="bold" color="white" alignItems="center" justifyContent="center">
{date}
</Text>
</Flex>
<Box>
{entries?.map((entry, indx) => (
<TimelineEntry
key={`${index + indx}_${new Date().getTime()}}`}
content={entry?.action}
time={entry?.createdOn}
/>
))}
</Box>
</Box>
))}
</Box>
);
}

export default Timeline;
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Flex, Text } from "@chakra-ui/react";
import { formatTime } from "utils/helpers";
import TimelineBullet from "./TimelineBullet";

export interface ITimelineEntryProps {
content: string | undefined;
time: string | undefined;
time: string | Date | undefined;
}

function TimelineEntry({ content, time }: ITimelineEntryProps) {
Expand All @@ -17,7 +18,7 @@ function TimelineEntry({ content, time }: ITimelineEntryProps) {
{content}
</Text>
<Text mt={1} mr={1} padding={1}>
{time}
{time ? formatTime(time) : ""}
</Text>
</Flex>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion react/src/hooks/useLabelIdentifierResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const useLabelIdentifierResolver = () => {
kind: ILabelIdentifierResolverResultKind.FAIL,
labelIdentifier,
error: err,
} as ILabelIdentifierResolvedValue),
}) as ILabelIdentifierResolvedValue,
);
setLoading(false);
return labelIdentifierResolvedValue;
Expand Down
6 changes: 3 additions & 3 deletions react/src/hooks/useScannedBoxesActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const useScannedBoxesActions = () => {
(data: IScannedBoxesData) =>
({
scannedBoxes: data.scannedBoxes.slice(0, -1),
} as IScannedBoxesData),
}) as IScannedBoxesData,
);
}, [apolloClient]);

Expand All @@ -84,7 +84,7 @@ export const useScannedBoxesActions = () => {
(data: IScannedBoxesData) =>
({
scannedBoxes: data.scannedBoxes.filter((box) => box.state === BoxState.InStock),
} as IScannedBoxesData),
}) as IScannedBoxesData,
);
}, [apolloClient]);

Expand All @@ -99,7 +99,7 @@ export const useScannedBoxesActions = () => {
scannedBoxes: data.scannedBoxes.filter(
(box) => !labelIdentifiers.includes(box.labelIdentifier),
),
} as IScannedBoxesData),
}) as IScannedBoxesData,
);
},
[apolloClient],
Expand Down
28 changes: 28 additions & 0 deletions react/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,31 @@ export const formatDateKey = (date: Date): string => {
return `${date.toLocaleString("default", { month: "short" })}
${date.getDate()}, ${date.getFullYear()}`;
};

export const prepareBoxHistoryEntryText = (text: string): string => {
// Remove the last character if it is a semicolon
const trimmedText = text?.endsWith(";") ? text?.slice(0, -1) : text;

// Replace "box state" with "box status" (ref. trello card https://trello.com/c/ClAikFIk)
const updatedText = trimmedText?.replace("box state", "box status");

return updatedText;
};

/**
* Formats a given date or string into a string representation of the time.
*
* @param {Date | string} date - The date or string to be formatted.
* @return {string} The formatted time as a string in the format "HH:MM".
*/
export const formatTime = (date: Date | string): string => {
const formattedDate = typeof date === "string" && date !== "" ? new Date(date) : date;

if (formattedDate instanceof Date) {
const hours = formattedDate.getHours().toString().padStart(2, "0");
const minutes = formattedDate.getMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
}

return "";
};
20 changes: 4 additions & 16 deletions react/src/views/Box/BoxView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { tags } from "mocks/tags";
import { selectOptionInSelectField, textContentMatcher } from "tests/helpers";
import BoxDetails from "./components/BoxDetails";
import { generateMockTransferAgreement } from "mocks/transferAgreements";
import { mockGraphQLError, mockNetworkError } from "mocks/functions";
import { mockGraphQLError, mockMatchMediaQuery, mockNetworkError } from "mocks/functions";
import { BOX_BY_LABEL_IDENTIFIER_AND_ALL_SHIPMENTS_QUERY } from "queries/queries";
import { organisation1 } from "mocks/organisations";
import { generateMockShipment, shipment1 } from "mocks/shipments";
Expand Down Expand Up @@ -307,21 +307,8 @@ const moveLocationOfBoxNetworkFailedMutation = {
};

beforeEach(() => {
// we need to mock matchmedia
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, "matchMedia", {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// setting the screensize to
mockMatchMediaQuery(true);
const mockedUseErrorHandling = jest.mocked(useErrorHandling);
mockedUseErrorHandling.mockReturnValue({ triggerError: mockedTriggerError });
const mockedUseNotification = jest.mocked(useNotification);
Expand Down Expand Up @@ -777,6 +764,7 @@ it("3.1.10 - No Data or Null Data Fetched for a given Box Label Identifier", asy
<BoxDetails
boxData={undefined}
boxInTransit={false}
onHistoryOpen={mockFunction}
onMoveToLocationClick={mockFunction}
onPlusOpen={mockFunction}
onMinusOpen={mockFunction}
Expand Down
46 changes: 45 additions & 1 deletion react/src/views/Box/BoxView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
UpdateStateMutation,
ClassicLocation,
ShipmentState,
User,
HistoryEntry,
} from "types/generated/graphql";
import {
ASSIGN_BOX_TO_DISTRIBUTION_MUTATION,
Expand All @@ -53,6 +55,10 @@ import { BoxViewSkeleton } from "components/Skeletons";

import { BoxReconciliationOverlay } from "components/BoxReconciliationOverlay/BoxReconciliationOverlay";
import { boxReconciliationOverlayVar } from "queries/cache";
import HistoryOverlay from "components/HistoryOverlay/HistoryOverlay";
import { ITimelineEntry } from "components/Timeline/Timeline";
import _ from "lodash";
import { formatDateKey, prepareBoxHistoryEntryText } from "utils/helpers";
import BoxDetails from "./components/BoxDetails";
import TakeItemsFromBoxOverlay from "./components/TakeItemsFromBoxOverlay";
import AddItemsToBoxOverlay from "./components/AddItemsToBoxOverlay";
Expand Down Expand Up @@ -146,7 +152,7 @@ function BTBox() {
const { globalPreferences } = useContext(GlobalPreferencesContext);
const currentBaseId = globalPreferences.selectedBase?.id;
const [currentBoxState, setCurrentState] = useState<BoxState | undefined>();

const { isOpen: isHistoryOpen, onOpen: onHistoryOpen, onClose: onHistoryClose } = useDisclosure();
const {
assignBoxesToShipment,
unassignBoxesToShipment,
Expand All @@ -169,6 +175,38 @@ function BTBox() {
? [BoxState.Receiving, BoxState.MarkedForShipment, BoxState.InTransit].includes(currentBoxState)
: false;

// map over each box HistoryEntry to compile its timeline records
const boxLogs: ITimelineEntry[] = (allData.data?.box?.history as HistoryEntry[])?.flatMap(
(histories) =>
_.compact([
histories?.user && {
action: prepareBoxHistoryEntryText(`${histories.user.name} ${histories.changes}`),
createdBy: histories.user as User,
createdOn: new Date(histories.changeDate),
},
]),
) as ITimelineEntry[];

const allLogs = _.orderBy(
_.sortBy(_.concat([...(boxLogs || [])]), "createdOn"),
["createdOn"],
["asc", "desc"],
);
const groupedHistoryEntries: _.Dictionary<ITimelineEntry[]> = _.groupBy(
allLogs,
(log) => `${formatDateKey(log?.createdOn)}`,
);

// sort each array of history entries in descending order
const sortedGroupedHistoryEntries = _(groupedHistoryEntries)
.toPairs()
.map(([date, entries]) => ({
date,
entries: _.orderBy(entries, (entry) => new Date(entry?.createdOn), "desc"),
}))
.orderBy((entry) => new Date(entry.date), "desc")
.value();

const [updateNumberOfItemsMutation, updateNumberOfItemsMutationStatus] = useMutation<
UpdateNumberOfItemsMutation,
UpdateNumberOfItemsMutationVariables
Expand Down Expand Up @@ -596,6 +634,7 @@ function BTBox() {
boxData={boxData}
boxInTransit={boxInTransit}
onPlusOpen={onPlusOpen}
onHistoryOpen={onHistoryOpen}
onMinusOpen={onMinusOpen}
onMoveToLocationClick={onMoveBoxToLocationClick}
onStateChange={onStateChange}
Expand Down Expand Up @@ -630,6 +669,11 @@ function BTBox() {
closeOnOverlayClick={false}
redirectToShipmentView
/>
<HistoryOverlay
isOpen={isHistoryOpen}
onClose={onHistoryClose}
data={sortedGroupedHistoryEntries}
/>
</VStack>
);
}
Expand Down
Loading

0 comments on commit e3f64f0

Please sign in to comment.