diff --git a/react/src/components/HistoryOverlay/HistoryOverlay.tsx b/react/src/components/HistoryOverlay/HistoryOverlay.tsx
new file mode 100644
index 000000000..ee3847dde
--- /dev/null
+++ b/react/src/components/HistoryOverlay/HistoryOverlay.tsx
@@ -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 (
+
+
+
+ Box History
+
+
+
+
+
+
+
+ );
+}
+
+export default HistoryOverlay;
diff --git a/react/src/components/Table/Filter.tsx b/react/src/components/Table/Filter.tsx
index b9bf183bf..d91c431a6 100644
--- a/react/src/components/Table/Filter.tsx
+++ b/react/src/components/Table/Filter.tsx
@@ -52,7 +52,7 @@ export function SelectColumnFilter({
({
label,
value: optionValues[label],
- } as ISelectOption),
+ }) as ISelectOption,
);
}, [id, preFilteredRows]);
diff --git a/react/src/components/Timeline/Timeline.tsx b/react/src/components/Timeline/Timeline.tsx
new file mode 100644
index 000000000..7b82437c0
--- /dev/null
+++ b/react/src/components/Timeline/Timeline.tsx
@@ -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 (
+
+ {records.map(({ date, entries }, index) => (
+
+
+
+ {date}
+
+
+
+ {entries?.map((entry, indx) => (
+
+ ))}
+
+
+ ))}
+
+ );
+}
+
+export default Timeline;
diff --git a/react/src/views/Transfers/ShipmentView/components/TimelineBullet.tsx b/react/src/components/Timeline/components/TimelineBullet.tsx
similarity index 100%
rename from react/src/views/Transfers/ShipmentView/components/TimelineBullet.tsx
rename to react/src/components/Timeline/components/TimelineBullet.tsx
diff --git a/react/src/views/Transfers/ShipmentView/components/TimelineEntry.tsx b/react/src/components/Timeline/components/TimelineEntry.tsx
similarity index 85%
rename from react/src/views/Transfers/ShipmentView/components/TimelineEntry.tsx
rename to react/src/components/Timeline/components/TimelineEntry.tsx
index effdc8d31..4d8d9d530 100644
--- a/react/src/views/Transfers/ShipmentView/components/TimelineEntry.tsx
+++ b/react/src/components/Timeline/components/TimelineEntry.tsx
@@ -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) {
@@ -17,7 +18,7 @@ function TimelineEntry({ content, time }: ITimelineEntryProps) {
{content}
- {time}
+ {time ? formatTime(time) : ""}
diff --git a/react/src/hooks/useLabelIdentifierResolver.ts b/react/src/hooks/useLabelIdentifierResolver.ts
index 3ca9c1b76..d937eb930 100644
--- a/react/src/hooks/useLabelIdentifierResolver.ts
+++ b/react/src/hooks/useLabelIdentifierResolver.ts
@@ -65,7 +65,7 @@ export const useLabelIdentifierResolver = () => {
kind: ILabelIdentifierResolverResultKind.FAIL,
labelIdentifier,
error: err,
- } as ILabelIdentifierResolvedValue),
+ }) as ILabelIdentifierResolvedValue,
);
setLoading(false);
return labelIdentifierResolvedValue;
diff --git a/react/src/hooks/useScannedBoxesActions.ts b/react/src/hooks/useScannedBoxesActions.ts
index a2e8883dd..3229cdd8d 100644
--- a/react/src/hooks/useScannedBoxesActions.ts
+++ b/react/src/hooks/useScannedBoxesActions.ts
@@ -72,7 +72,7 @@ export const useScannedBoxesActions = () => {
(data: IScannedBoxesData) =>
({
scannedBoxes: data.scannedBoxes.slice(0, -1),
- } as IScannedBoxesData),
+ }) as IScannedBoxesData,
);
}, [apolloClient]);
@@ -84,7 +84,7 @@ export const useScannedBoxesActions = () => {
(data: IScannedBoxesData) =>
({
scannedBoxes: data.scannedBoxes.filter((box) => box.state === BoxState.InStock),
- } as IScannedBoxesData),
+ }) as IScannedBoxesData,
);
}, [apolloClient]);
@@ -99,7 +99,7 @@ export const useScannedBoxesActions = () => {
scannedBoxes: data.scannedBoxes.filter(
(box) => !labelIdentifiers.includes(box.labelIdentifier),
),
- } as IScannedBoxesData),
+ }) as IScannedBoxesData,
);
},
[apolloClient],
diff --git a/react/src/utils/helpers.ts b/react/src/utils/helpers.ts
index 37fa36f22..82b799c8e 100644
--- a/react/src/utils/helpers.ts
+++ b/react/src/utils/helpers.ts
@@ -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 "";
+};
diff --git a/react/src/views/Box/BoxView.test.tsx b/react/src/views/Box/BoxView.test.tsx
index 3103e565b..ca8a88d13 100644
--- a/react/src/views/Box/BoxView.test.tsx
+++ b/react/src/views/Box/BoxView.test.tsx
@@ -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";
@@ -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);
@@ -777,6 +764,7 @@ it("3.1.10 - No Data or Null Data Fetched for a given Box Label Identifier", asy
();
-
+ const { isOpen: isHistoryOpen, onOpen: onHistoryOpen, onClose: onHistoryClose } = useDisclosure();
const {
assignBoxesToShipment,
unassignBoxesToShipment,
@@ -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 = _.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
@@ -596,6 +634,7 @@ function BTBox() {
boxData={boxData}
boxInTransit={boxInTransit}
onPlusOpen={onPlusOpen}
+ onHistoryOpen={onHistoryOpen}
onMinusOpen={onMinusOpen}
onMoveToLocationClick={onMoveBoxToLocationClick}
onStateChange={onStateChange}
@@ -630,6 +669,11 @@ function BTBox() {
closeOnOverlayClick={false}
redirectToShipmentView
/>
+
);
}
diff --git a/react/src/views/Box/BoxViewHistoryOverlay.test.tsx b/react/src/views/Box/BoxViewHistoryOverlay.test.tsx
new file mode 100644
index 000000000..54563dc75
--- /dev/null
+++ b/react/src/views/Box/BoxViewHistoryOverlay.test.tsx
@@ -0,0 +1,127 @@
+/* eslint-disable */
+import "@testing-library/jest-dom";
+import { screen, render } from "tests/test-utils";
+import userEvent from "@testing-library/user-event";
+import { cache } from "queries/cache";
+
+import { useErrorHandling } from "hooks/useErrorHandling";
+import { useNotification } from "hooks/useNotification";
+
+import { BOX_BY_LABEL_IDENTIFIER_AND_ALL_SHIPMENTS_QUERY } from "queries/queries";
+import { organisation1 } from "mocks/organisations";
+import BTBox from "./BoxView";
+
+import { BoxState } from "types/generated/graphql";
+import { history1, history2 } from "mocks/histories";
+import { generateMockBox } from "mocks/boxes";
+import { mockMatchMediaQuery } from "mocks/functions";
+
+const mockedTriggerError = jest.fn();
+const mockedCreateToast = jest.fn();
+jest.mock("hooks/useErrorHandling");
+jest.mock("hooks/useNotification");
+
+cache.reset();
+
+const initialQueryForBoxWithHistory = {
+ request: {
+ query: BOX_BY_LABEL_IDENTIFIER_AND_ALL_SHIPMENTS_QUERY,
+ variables: {
+ labelIdentifier: "123",
+ },
+ notifyOnNetworkStatusChange: true,
+ },
+ result: {
+ data: {
+ box: generateMockBox({
+ labelIdentifier: "123",
+ state: BoxState.InStock,
+ histories: [history1, history2],
+ }),
+ },
+ },
+};
+
+// Test case 3.1.12
+describe("3.1.12 - Box HistoryOverlay on BoxView", () => {
+ beforeEach(() => {
+ // setting the screensize to
+ mockMatchMediaQuery(true);
+ const mockedUseErrorHandling = jest.mocked(useErrorHandling);
+ mockedUseErrorHandling.mockReturnValue({ triggerError: mockedTriggerError });
+ const mockedUseNotification = jest.mocked(useNotification);
+ mockedUseNotification.mockReturnValue({ createToast: mockedCreateToast });
+ });
+
+ // Test case 3.1.12.1
+ it("3.1.12.1 - When more than one entry is available displays history icon", async () => {
+ render(, {
+ routePath: "/bases/:baseId/boxes/:labelIdentifier",
+ initialUrl: "/bases/1/boxes/123",
+ additionalRoute: "/bases/1/shipment/1",
+ mocks: [initialQueryForBoxWithHistory],
+ addTypename: true,
+ cache,
+ globalPreferences: {
+ dispatch: jest.fn(),
+ globalPreferences: {
+ organisation: { id: organisation1.id, name: organisation1.name },
+ availableBases: organisation1.bases,
+ selectedBase: organisation1.bases[0],
+ },
+ },
+ });
+
+ const heading = await screen.findByRole("heading", { name: /box 123/i });
+ expect(heading).toBeInTheDocument();
+
+ const showHistoryButton = await screen.findByRole("button", {
+ name: /show detail history/i,
+ });
+
+ expect(showHistoryButton).toBeInTheDocument();
+ }, 10000);
+
+ // Test case 3.1.12.2
+ it("3.1.12.2 - Click on history icons opens history overlay", async () => {
+ const user = userEvent.setup();
+
+ render(, {
+ routePath: "/bases/:baseId/boxes/:labelIdentifier",
+ initialUrl: "/bases/1/boxes/123",
+ additionalRoute: "/bases/1/shipment/1",
+ mocks: [initialQueryForBoxWithHistory],
+ addTypename: true,
+ cache,
+ globalPreferences: {
+ dispatch: jest.fn(),
+ globalPreferences: {
+ organisation: { id: organisation1.id, name: organisation1.name },
+ availableBases: organisation1.bases,
+ selectedBase: organisation1.bases[0],
+ },
+ },
+ });
+
+ const heading = await screen.findByRole("heading", { name: /box 123/i });
+ expect(heading).toBeInTheDocument();
+
+ const historyButton = await screen.findByRole("button", {
+ name: /show detail history/i,
+ });
+
+ expect(historyButton).toBeInTheDocument();
+
+ await user.click(historyButton);
+
+ const banner = await screen.findByRole("banner");
+
+ expect(banner).toBeInTheDocument();
+ expect(screen.getByText(/jan 14, 2023/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/dev coordinator changed box location from wh men to wh women/i),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/jan 12, 2023/i)).toBeInTheDocument();
+ expect(screen.getByText(/dev coordinator created record/i)).toBeInTheDocument();
+ }, 10000);
+});
diff --git a/react/src/views/Box/BoxViewReconciliationOverlay.test.tsx b/react/src/views/Box/BoxViewReconciliationOverlay.test.tsx
index 71296b46c..2bebb9101 100644
--- a/react/src/views/Box/BoxViewReconciliationOverlay.test.tsx
+++ b/react/src/views/Box/BoxViewReconciliationOverlay.test.tsx
@@ -18,6 +18,7 @@ import { products } from "mocks/products";
import { tag1, tag2 } from "mocks/tags";
import { generateMockShipment } from "mocks/shipments";
import { ShipmentState } from "types/generated/graphql";
+import { mockMatchMediaQuery } from "mocks/functions";
const mockedTriggerError = jest.fn();
const mockedCreateToast = jest.fn();
@@ -126,21 +127,8 @@ const queryShipmentDetailForBoxReconciliation = {
};
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);
diff --git a/react/src/views/Box/components/BoxCard.tsx b/react/src/views/Box/components/BoxCard.tsx
index 3ab1a9416..8a8079286 100644
--- a/react/src/views/Box/components/BoxCard.tsx
+++ b/react/src/views/Box/components/BoxCard.tsx
@@ -22,7 +22,9 @@ import {
SkeletonCircle,
Skeleton,
SkeletonText,
+ Icon,
} from "@chakra-ui/react";
+import { MdHistory } from "react-icons/md";
import { NavLink } from "react-router-dom";
import {
BoxByLabelIdentifierQuery,
@@ -38,6 +40,7 @@ import HistoryEntries from "./HistoryEntries";
export interface IBoxCardProps {
boxData: BoxByLabelIdentifierQuery["box"] | UpdateLocationOfBoxMutation["updateBox"];
boxInTransit: boolean;
+ onHistoryOpen: () => void;
onPlusOpen: () => void;
onMinusOpen: () => void;
onStateChange: (boxState: BoxState) => void;
@@ -47,6 +50,7 @@ export interface IBoxCardProps {
function BoxCard({
boxData,
boxInTransit,
+ onHistoryOpen,
onPlusOpen,
onMinusOpen,
onStateChange,
@@ -333,12 +337,28 @@ function BoxCard({
History:
- {!isLoading && (
-
- )}
- {isLoading && (
-
- )}
+
+ {!isLoading && (
+
+ )}
+ {isLoading && (
+
+ )}
+ {boxData?.history && boxData?.history?.length > 1 && (
+ <>
+
+ }
+ />
+ >
+ )}
+
>
diff --git a/react/src/views/Box/components/BoxDetails.tsx b/react/src/views/Box/components/BoxDetails.tsx
index 17bfca5bc..71377b6ab 100644
--- a/react/src/views/Box/components/BoxDetails.tsx
+++ b/react/src/views/Box/components/BoxDetails.tsx
@@ -13,6 +13,7 @@ interface IBoxDetailsProps {
boxData: BoxByLabelIdentifierQuery["box"] | UpdateLocationOfBoxMutation["updateBox"];
boxInTransit: boolean;
onMoveToLocationClick: (locationId: string) => void;
+ onHistoryOpen: () => void;
onPlusOpen: () => void;
onMinusOpen: () => void;
onStateChange: (boxState: BoxState) => void;
@@ -30,6 +31,7 @@ function BoxDetails({
onMoveToLocationClick,
onAssignBoxToDistributionEventClick,
onUnassignBoxFromDistributionEventClick,
+ onHistoryOpen,
onPlusOpen,
onMinusOpen,
onStateChange,
@@ -53,6 +55,7 @@ function BoxDetails({
{historyEntry?.user?.name}
{" on "}
{formatDate(historyEntry?.changeDate)}{" "}
- {prepareHistoryEntryText(historyEntry?.changes)}
+ {prepareBoxHistoryEntryText(historyEntry?.changes)}
diff --git a/react/src/views/BoxEdit/BoxEditView.tsx b/react/src/views/BoxEdit/BoxEditView.tsx
index 3b06f53ba..09523413b 100644
--- a/react/src/views/BoxEdit/BoxEditView.tsx
+++ b/react/src/views/BoxEdit/BoxEditView.tsx
@@ -135,7 +135,7 @@ function BoxEditView() {
numberOfItems: boxEditFormData.numberOfItems,
locationId: parseInt(boxEditFormData.locationId.value, 10),
tagIds,
- comment: boxEditFormData?.comment,
+ comment: boxEditFormData?.comment || null,
},
})
.then((mutationResult) => {
diff --git a/react/src/views/Transfers/CreateShipment/CreateShipmentView.tsx b/react/src/views/Transfers/CreateShipment/CreateShipmentView.tsx
index e6b8936b5..fad51af92 100644
--- a/react/src/views/Transfers/CreateShipment/CreateShipmentView.tsx
+++ b/react/src/views/Transfers/CreateShipment/CreateShipmentView.tsx
@@ -144,7 +144,7 @@ function CreateShipmentView() {
id: agreement.id,
name: agreement.name,
bases: agreement.bases,
- } as IOrganisationBaseData),
+ }) as IOrganisationBaseData,
)
.reduce((accumulator, currentOrg) => {
// Merge options. If there are multiple transfer agreements this step is necessary
diff --git a/react/src/views/Transfers/ShipmentView/ShipmentView.test.tsx b/react/src/views/Transfers/ShipmentView/ShipmentView.test.tsx
index a606a851d..5a99590b9 100644
--- a/react/src/views/Transfers/ShipmentView/ShipmentView.test.tsx
+++ b/react/src/views/Transfers/ShipmentView/ShipmentView.test.tsx
@@ -12,6 +12,7 @@ import { BoxState, ShipmentState } from "types/generated/graphql";
import { useErrorHandling } from "hooks/useErrorHandling";
import { useNotification } from "hooks/useNotification";
import userEvent from "@testing-library/user-event";
+import { mockMatchMediaQuery } from "mocks/functions";
import ShipmentView, { SHIPMENT_BY_ID_QUERY } from "./ShipmentView";
const mockedTriggerError = jest.fn();
@@ -154,21 +155,8 @@ const initialRecevingUIAsTargetOrgQuery = {
};
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);
@@ -177,21 +165,8 @@ beforeEach(() => {
describe("4.5 Test Cases", () => {
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);
});
// Test case 4.5.1
diff --git a/react/src/views/Transfers/ShipmentView/ShipmentView.tsx b/react/src/views/Transfers/ShipmentView/ShipmentView.tsx
index 5e97ab864..a3e855bf2 100644
--- a/react/src/views/Transfers/ShipmentView/ShipmentView.tsx
+++ b/react/src/views/Transfers/ShipmentView/ShipmentView.tsx
@@ -45,13 +45,27 @@ import { BoxReconciliationOverlay } from "components/BoxReconciliationOverlay/Bo
import { UPDATE_SHIPMENT_WHEN_RECEIVING } from "queries/mutations";
import { boxReconciliationOverlayVar } from "queries/cache";
import { MobileBreadcrumbButton } from "components/BreadcrumbNavigation";
+import { ITimelineEntry } from "components/Timeline/Timeline";
import ShipmentCard from "./components/ShipmentCard";
-import ShipmentTabs, { IShipmentHistory, ShipmentActionEvent } from "./components/ShipmentTabs";
+import ShipmentTabs from "./components/ShipmentTabs";
import ShipmentOverlay, { IShipmentOverlayData } from "./components/ShipmentOverlay";
import ShipmentActionButtons from "./components/ShipmentActionButtons";
import ShipmentReceivingContent from "./components/ShipmentReceivingContent";
import ShipmentReceivingCard from "./components/ShipmentReceivingCard";
+// eslint-disable-next-line no-shadow
+enum ShipmentActionEvent {
+ ShipmentStarted = "Shipment Started",
+ ShipmentCanceled = "Shipment Canceled",
+ ShipmentSent = "Shipment Sent",
+ ShipmentStartReceiving = "Shipment Being Received",
+ ShipmentCompleted = "Shipment Completed",
+ BoxAdded = "Box Added",
+ BoxRemoved = "Box Removed",
+ BoxLost = "Box Marked Lost",
+ BoxReceived = "Box Received",
+}
+
// graphql query and mutations
export const SHIPMENT_BY_ID_QUERY = gql`
${SHIPMENT_FIELDS_FRAGMENT}
@@ -123,7 +137,6 @@ function ShipmentView() {
onClose: onShipmentOverlayClose,
onOpen: onShipmentOverlayOpen,
} = useDisclosure();
-
// State to show minus button near boxes when remove button is triggered
const [showRemoveIcon, setShowRemoveIcon] = useState(false);
const [shipmentState, setShipmentState] = useState();
@@ -362,10 +375,32 @@ function ShipmentView() {
const shipmentContents = (data?.shipment?.details.filter((item) => item.removedOn === null) ??
[]) as ShipmentDetail[];
+ const changesLabel = (history: any): string => {
+ let changes = "";
+ if (
+ [
+ ShipmentActionEvent.ShipmentCanceled,
+ ShipmentActionEvent.ShipmentCompleted,
+ ShipmentActionEvent.ShipmentSent,
+ ShipmentActionEvent.ShipmentStartReceiving,
+ ShipmentActionEvent.ShipmentStarted,
+ ].includes(history.action)
+ ) {
+ changes = `Shipment is ${history.action.toLowerCase().replace("shipment", "")} by ${history
+ .createdBy?.name}`;
+ } else {
+ changes = `Box ${history.box} is ${history.action
+ .toLowerCase()
+ .replace("box", "")} by ${history.createdBy?.name}`;
+ }
+
+ return changes;
+ };
+
const generateShipmentHistory = (
entry: Partial>,
- ): IShipmentHistory[] => {
- const shipmentHistory: IShipmentHistory[] = [];
+ ): ITimelineEntry[] => {
+ const shipmentHistory: ITimelineEntry[] = [];
Object.entries(entry).forEach(([action, shipmentObj]) => {
if (shipmentObj.createdOn) {
@@ -380,7 +415,7 @@ function ShipmentView() {
return shipmentHistory;
};
- const shipmentLogs: IShipmentHistory[] = generateShipmentHistory({
+ const shipmentLogs: ITimelineEntry[] = generateShipmentHistory({
[ShipmentActionEvent.ShipmentStarted]: {
createdOn: shipmentData?.startedOn,
createdBy: shipmentData?.startedBy! as User,
@@ -403,10 +438,7 @@ function ShipmentView() {
},
});
- // map over each ShipmentDetail to compile its history records
- const shipmentDetailLogs: IShipmentHistory[] = (
- data?.shipment?.details! as ShipmentDetail[]
- )?.flatMap((detail) =>
+ const shipmentDetailLogs = (data?.shipment?.details! as ShipmentDetail[])?.flatMap((detail) =>
_.compact([
detail?.createdBy && {
box: detail.box.labelIdentifier,
@@ -433,29 +465,30 @@ function ShipmentView() {
createdOn: new Date(detail?.receivedOn),
},
]),
- ) as unknown as IShipmentHistory[];
+ );
const allLogs = _.orderBy(
_.sortBy(_.concat([...(shipmentLogs || []), ...(shipmentDetailLogs || [])]), "createdOn"),
["createdOn"],
["asc", "desc"],
);
- const groupedHistoryEntries: _.Dictionary = _.groupBy(
+ const groupedHistoryEntries: _.Dictionary = _.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"),
+ entries: _.orderBy(entries, (entry) => new Date(entry?.createdOn), "desc").map((entry) => ({
+ ...entry,
+ action: changesLabel(entry),
+ })),
}))
.orderBy((entry) => new Date(entry.date), "desc")
.value();
- // variables for loading dynamic components
let shipmentTitle = View Shipment;
let shipmentTab;
let shipmentCard;
@@ -465,7 +498,6 @@ function ShipmentView() {
let canLooseShipment = false;
let shipmentActionButtons = ;
- // error and loading handling
if (error) {
shipmentTab = (
@@ -484,7 +516,6 @@ function ShipmentView() {
(b) => b.id === data?.shipment?.sourceBase?.id,
) !== "undefined";
- // Role Sender // Different State UI Changes
if (ShipmentState.Preparing === shipmentState && isSender) {
canUpdateShipment = true;
canCancelShipment = true;
@@ -492,9 +523,7 @@ function ShipmentView() {
shipmentTitle = Prepare Shipment;
} else if (ShipmentState.Sent === shipmentState && isSender) {
canLooseShipment = true;
- }
- // Role Receiver // Different State UI Changes
- else if (ShipmentState.Sent === shipmentState && !isSender) {
+ } else if (ShipmentState.Sent === shipmentState && !isSender) {
canLooseShipment = true;
} else if (ShipmentState.Receiving === shipmentState && !isSender) {
canLooseShipment = true;
diff --git a/react/src/views/Transfers/ShipmentView/components/ShipmentHistory.tsx b/react/src/views/Transfers/ShipmentView/components/ShipmentHistory.tsx
deleted file mode 100644
index 6582595c3..000000000
--- a/react/src/views/Transfers/ShipmentView/components/ShipmentHistory.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Box, Flex, Text } from "@chakra-ui/react";
-import { IGroupedHistoryEntry, IShipmentHistory, ShipmentActionEvent } from "./ShipmentTabs";
-import TimelineEntry from "./TimelineEntry";
-
-export interface IShipmentHistoryProps {
- histories: IGroupedHistoryEntry[];
-}
-
-function ShipmentHistory({ histories }: IShipmentHistoryProps) {
- const changesLabel = (history: IShipmentHistory): string => {
- let changes = "";
- if (
- [
- ShipmentActionEvent.ShipmentCanceled,
- ShipmentActionEvent.ShipmentCompleted,
- ShipmentActionEvent.ShipmentSent,
- ShipmentActionEvent.ShipmentStartReceiving,
- ShipmentActionEvent.ShipmentStarted,
- ].includes(history.action)
- ) {
- changes = `Shipment is ${history.action.toLowerCase().replace("shipment", "")} by ${
- history.createdBy?.name
- }`;
- } else {
- changes = `Box ${history.box} is ${history.action.toLowerCase().replace("box", "")} by ${
- history.createdBy?.name
- }`;
- }
-
- return changes;
- };
-
- function formatTime(date: Date): string {
- const hours = date.getHours().toString().padStart(2, "0");
- const minutes = date.getMinutes().toString().padStart(2, "0");
- return `${hours}:${minutes}`;
- }
-
- return (
-
- {histories.map(({ date, entries }, index) => (
-
-
-
- {date}
-
-
-
- {entries?.map((entry, indx) => (
-
- ))}
-
-
- ))}
-
- );
-}
-
-export default ShipmentHistory;
diff --git a/react/src/views/Transfers/ShipmentView/components/ShipmentTabs.tsx b/react/src/views/Transfers/ShipmentView/components/ShipmentTabs.tsx
index 30e98f918..545d93a26 100644
--- a/react/src/views/Transfers/ShipmentView/components/ShipmentTabs.tsx
+++ b/react/src/views/Transfers/ShipmentView/components/ShipmentTabs.tsx
@@ -1,8 +1,8 @@
import { TabList, TabPanels, Tabs, TabPanel, Tab, Center } from "@chakra-ui/react";
+import ShipmentHistory, { IGroupedRecordEntry } from "components/Timeline/Timeline";
import _ from "lodash";
import { Box, ShipmentDetail, ShipmentState, User } from "types/generated/graphql";
import ShipmentContent, { IShipmentContent } from "./ShipmentContent";
-import ShipmentHistory from "./ShipmentHistory";
// eslint-disable-next-line no-shadow
export enum ShipmentActionEvent {
@@ -32,7 +32,7 @@ export interface IGroupedHistoryEntry {
export interface IShipmentTabsProps {
shipmentState: ShipmentState | undefined;
detail: ShipmentDetail[];
- histories: IGroupedHistoryEntry[];
+ histories: IGroupedRecordEntry[];
isLoadingMutation: boolean | undefined;
showRemoveIcon: Boolean;
onRemoveBox: (id: string) => void;
@@ -62,7 +62,7 @@ function ShipmentTabs({
size: group[0]?.sourceSize,
numberOfItems: shipment.sourceQuantity,
product: group[0]?.sourceProduct,
- } as Box),
+ }) as Box,
),
}))
.orderBy((value) => value.totalLosts, "asc")
@@ -95,7 +95,7 @@ function ShipmentTabs({
/>
-
+