From 8c05d49af6b11c421d1dd098cf91a1955ed94992 Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Wed, 5 Feb 2025 11:31:16 -0700 Subject: [PATCH 1/6] issue #421: loading button --- .../__tests__/ConfirmationPage.test.tsx | 19 +---- src/app/label-data-confirmation/page.tsx | 80 +++++-------------- src/app/page.tsx | 21 +++-- src/components/AuthComponents/LoginModal.tsx | 28 +++---- src/components/AuthComponents/SignUpModal.tsx | 31 ++++--- src/components/LoadingButton.tsx | 33 ++++++++ src/components/QuantityChip.tsx | 28 +++++++ .../HorizontalNonLinearStepper.test.tsx | 4 +- .../__tests__/LoadingButton.test.tsx | 52 ++++++++++++ .../__tests__/QuantityChip.test.tsx | 19 +++++ src/components/stepper.tsx | 19 +++-- 11 files changed, 213 insertions(+), 121 deletions(-) create mode 100644 src/components/LoadingButton.tsx create mode 100644 src/components/QuantityChip.tsx create mode 100644 src/components/__tests__/LoadingButton.test.tsx create mode 100644 src/components/__tests__/QuantityChip.test.tsx diff --git a/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx b/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx index 8b0ecbdf..0e93ce6d 100644 --- a/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx +++ b/src/app/label-data-confirmation/__tests__/ConfirmationPage.test.tsx @@ -1,14 +1,13 @@ import FileUploaded from "@/classe/File"; import useUploadedFilesStore from "@/stores/fileStore"; import useLabelDataStore from "@/stores/labelDataStore"; -import { Quantity } from "@/types/types"; import { VERIFIED_LABEL_DATA, VERIFIED_LABEL_DATA_WITH_ID, } from "@/utils/client/constants"; import { fireEvent, render, screen } from "@testing-library/react"; import axios from "axios"; -import LabelDataConfirmationPage, { QuantityChips } from "../page"; +import LabelDataConfirmationPage from "../page"; const mockedRouterPush = jest.fn(); jest.mock("next/navigation", () => ({ @@ -206,19 +205,3 @@ describe("LabelDataConfirmationPage", () => { expect(bilingualTableContainers.length).toBe(4); }); }); - -describe("QuantityChips", () => { - it("renders valid quantities, filters out invalid values", () => { - const quantities: Quantity[] = [ - { value: "5", unit: "kg" }, - { value: "", unit: "g" }, - { value: "0", unit: "kg" }, - ]; - - render(); - - expect(screen.getByText("5 kg")).toBeInTheDocument(); - expect(screen.getByText("0 kg")).toBeInTheDocument(); - expect(screen.queryByText("g")).not.toBeInTheDocument(); - }); -}); diff --git a/src/app/label-data-confirmation/page.tsx b/src/app/label-data-confirmation/page.tsx index aecd81e9..cee0c083 100644 --- a/src/app/label-data-confirmation/page.tsx +++ b/src/app/label-data-confirmation/page.tsx @@ -1,17 +1,16 @@ "use client"; import ImageViewer from "@/components/ImageViewer"; +import LoadingButton from "@/components/LoadingButton"; +import { QuantityChips } from "@/components/QuantityChip"; import useAlertStore from "@/stores/alertStore"; import useUploadedFilesStore from "@/stores/fileStore"; import useLabelDataStore from "@/stores/labelDataStore"; -import { BilingualField, LabelData, Quantity } from "@/types/types"; +import { BilingualField, LabelData } from "@/types/types"; import { processAxiosError } from "@/utils/client/apiErrors"; import { isAllVerified } from "@/utils/client/fieldValidation"; import { Box, - Button, Checkbox, - Chip, - CircularProgress, Container, FormControlLabel, FormGroup, @@ -40,7 +39,8 @@ const LabelDataConfirmationPage = () => { ); const imageFiles = uploadedFiles.map((file) => file.getFile()); const router = useRouter(); - const [loading, setLoading] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [editLoading, setEditLoading] = useState(false); const showAlert = useAlertStore((state) => state.showAlert); const [confirmed, setConfirmed] = useState(false); const { t } = useTranslation("confirmationPage"); @@ -51,7 +51,7 @@ const LabelDataConfirmationPage = () => { const putLabelData = (labelData: LabelData, signal: AbortSignal) => { const confirmedLabelData = { ...labelData, confirmed: true }; - setLoading(true); + setConfirmLoading(true); axios .put( `/api-next/inspections/${confirmedLabelData.inspectionId}`, @@ -74,7 +74,7 @@ const LabelDataConfirmationPage = () => { ); }) .finally(() => { - setLoading(false); + setConfirmLoading(false); }); }; @@ -85,7 +85,7 @@ const LabelDataConfirmationPage = () => { formData.append("files", file); }); formData.append("labelData", JSON.stringify(labelData)); - setLoading(true); + setConfirmLoading(true); axios .post("/api-next/inspections", formData, { headers: { Authorization: getAuthHeader() }, @@ -105,11 +105,12 @@ const LabelDataConfirmationPage = () => { ); }) .finally(() => { - setLoading(false); + setConfirmLoading(false); }); }; const handleEditClick = () => { + setEditLoading(true); if (labelData?.inspectionId) { router.push(`/label-data-validation/${labelData.inspectionId}`); } else { @@ -512,7 +513,7 @@ const LabelDataConfirmationPage = () => { setConfirmed(event.target.checked)} - disabled={loading} + disabled={confirmLoading} data-testid="confirmation-checkbox" /> } @@ -526,38 +527,24 @@ const LabelDataConfirmationPage = () => { {/* Confirm and Edit Buttons */} - - + loading={editLoading} + text={t("confirmationSection.editButton")} + /> @@ -568,31 +555,6 @@ const LabelDataConfirmationPage = () => { export default LabelDataConfirmationPage; -export interface QuantityChipsProps extends React.ComponentProps { - quantities: Quantity[] | undefined; -} - -export const QuantityChips = React.forwardRef< - HTMLDivElement, - QuantityChipsProps ->(({ quantities, ...rest }, ref) => { - return ( - - {quantities - ?.filter((q) => q.value) - .map((q, i) => ( - - ))} - - ); -}); - -QuantityChips.displayName = "QuantityChips"; - interface BilingualTableProps { data: BilingualField[]; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 10f53823..876fdfad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,10 @@ "use client"; import Dropzone from "@/components/Dropzone"; +import LoadingButton from "@/components/LoadingButton"; import FileList from "@/components/FileList"; import useUploadedFilesStore from "@/stores/fileStore"; import type { DropzoneState } from "@/types/types"; -import { Box, Button, Grid2, Tooltip } from "@mui/material"; +import { Box, Grid2, Tooltip } from "@mui/material"; import { useRouter } from "next/navigation"; import { Suspense, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -11,6 +12,7 @@ import { useTranslation } from "react-i18next"; function HomePage() { const { t } = useTranslation("homePage"); const router = useRouter(); + const [loading, setLoading] = useState(false); const [dropzoneState, setDropzoneState] = useState({ visible: false, @@ -19,6 +21,11 @@ function HomePage() { }); const { uploadedFiles } = useUploadedFilesStore(); + const handleSubmission = () => { + setLoading(true); + router.push("/label-data-validation"); + }; + return ( @@ -61,17 +68,17 @@ function HomePage() { className="w-[90%] max-w-full min-w-[133.44px]" > - + onClick={handleSubmission} + loading={loading} + text={t("submitButton")} + /> diff --git a/src/components/AuthComponents/LoginModal.tsx b/src/components/AuthComponents/LoginModal.tsx index 4384179e..8d2a89b9 100644 --- a/src/components/AuthComponents/LoginModal.tsx +++ b/src/components/AuthComponents/LoginModal.tsx @@ -1,10 +1,11 @@ -import { Box, Button, Modal, Typography } from "@mui/material"; -import AccountCircleIcon from "@mui/icons-material/AccountCircle"; -import LockIcon from "@mui/icons-material/Lock"; import theme from "@/app/theme"; import IconInput from "@/components/IconInput"; +import AccountCircleIcon from "@mui/icons-material/AccountCircle"; +import LockIcon from "@mui/icons-material/Lock"; +import { Box, Modal, Typography } from "@mui/material"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import LoadingButton from "../LoadingButton"; interface LoginProps { isOpen: boolean; @@ -17,11 +18,14 @@ const LoginModal = ({ isOpen, login, onChangeMode }: LoginProps) => { const [password, setPassword] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const { t } = useTranslation("authentication"); + const [loading, setLoading] = useState(false); const handleSubmit = () => { + setLoading(true); login(username, password).then((message) => { console.log(message); setErrorMessage(message); + setLoading(false); }); }; @@ -104,18 +108,14 @@ const LoginModal = ({ isOpen, login, onChangeMode }: LoginProps) => { > {errorMessage} - + disabled={username === "" || password === ""} + loading={loading} + text={t("login.title")} + className="!bg-white !pointer-events-auto" + data-testid="modal-submit" + /> { const [checkedReminder, setReminderChecked] = useState(false); const [errorMessage, setErrorMessage] = useState(""); const { t } = useTranslation("authentication"); + const [loading, setLoading] = useState(false); const handleSubmit = () => { + setLoading(true); signup(username, password, confirmPassword).then((message) => { setErrorMessage(message); + setLoading(false); }); }; @@ -146,23 +149,19 @@ const SignUpModal = ({ isOpen, signup, onChangeMode }: SignUpProps) => { > {errorMessage} - + loading={loading} + text={t("signup.title")} + className="!bg-white !pointer-events-auto" + data-testid="modal-submit" + /> = ({ + loading, + text, + disabled, + children, + ...props +}) => { + return ( + + ); +}; + +export default LoadingButton; diff --git a/src/components/QuantityChip.tsx b/src/components/QuantityChip.tsx new file mode 100644 index 00000000..edc6c9f6 --- /dev/null +++ b/src/components/QuantityChip.tsx @@ -0,0 +1,28 @@ +import { Quantity } from "@/types/types"; +import { Box, Chip } from "@mui/material"; +import React from "react"; + +export interface QuantityChipsProps extends React.ComponentProps { + quantities: Quantity[] | undefined; +} + +export const QuantityChips = React.forwardRef< + HTMLDivElement, + QuantityChipsProps +>(({ quantities, ...rest }, ref) => { + return ( + + {quantities + ?.filter((q) => q.value) + .map((q, i) => ( + + ))} + + ); +}); + +QuantityChips.displayName = "QuantityChips"; diff --git a/src/components/__tests__/HorizontalNonLinearStepper.test.tsx b/src/components/__tests__/HorizontalNonLinearStepper.test.tsx index 7c4a8176..7a62bb1e 100644 --- a/src/components/__tests__/HorizontalNonLinearStepper.test.tsx +++ b/src/components/__tests__/HorizontalNonLinearStepper.test.tsx @@ -159,7 +159,9 @@ describe("HorizontalNonLinearStepper with StepperControls", () => { />, ); const submitButton = screen.getByText("stepper.submit"); - expect(submitButton).toBeDisabled(); + setTimeout(() => { + expect(submitButton).toBeDisabled(); + }, 350); }); it("enables 'Submit' button when all steps are completed", () => { diff --git a/src/components/__tests__/LoadingButton.test.tsx b/src/components/__tests__/LoadingButton.test.tsx new file mode 100644 index 00000000..5d3b9a0a --- /dev/null +++ b/src/components/__tests__/LoadingButton.test.tsx @@ -0,0 +1,52 @@ +import { ButtonProps } from "@mui/material"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import LoadingButton from "../LoadingButton"; + +describe("LoadingButton", () => { + const defaultProps: ButtonProps & { loading: boolean; text: string } = { + loading: false, + text: "Click me", + }; + + it("renders the button with provided text", () => { + render(); + expect(screen.getByText("Click me")).toBeInTheDocument(); + }); + + it("renders children if provided", () => { + render( + + Custom Child + , + ); + expect(screen.getByText("Custom Child")).toBeInTheDocument(); + }); + + it("disables button when loading is true", () => { + render(); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("disables button when disabled prop is true", () => { + render(); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("shows loading spinner when loading is true", () => { + render(); + expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); + }); + + it("does not show spinner when loading is false", () => { + render(); + expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument(); + }); + + it("fires onClick when clicked", async () => { + const onClick = jest.fn(); + render(); + await userEvent.click(screen.getByRole("button")); + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/components/__tests__/QuantityChip.test.tsx b/src/components/__tests__/QuantityChip.test.tsx new file mode 100644 index 00000000..2f089320 --- /dev/null +++ b/src/components/__tests__/QuantityChip.test.tsx @@ -0,0 +1,19 @@ +import { Quantity } from "@/types/types"; +import { render, screen } from "@testing-library/react"; +import { QuantityChips } from "../QuantityChip"; + +describe("QuantityChips", () => { + it("renders valid quantities, filters out invalid values", () => { + const quantities: Quantity[] = [ + { value: "5", unit: "kg" }, + { value: "", unit: "g" }, + { value: "0", unit: "kg" }, + ]; + + render(); + + expect(screen.getByText("5 kg")).toBeInTheDocument(); + expect(screen.getByText("0 kg")).toBeInTheDocument(); + expect(screen.queryByText("g")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/stepper.tsx b/src/components/stepper.tsx index 85d6610b..5c8ed530 100644 --- a/src/components/stepper.tsx +++ b/src/components/stepper.tsx @@ -4,8 +4,9 @@ import Step from "@mui/material/Step"; import StepButton from "@mui/material/StepButton"; import StepLabel from "@mui/material/StepLabel"; import Stepper from "@mui/material/Stepper"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import LoadingButton from "./LoadingButton"; export enum StepStatus { Incomplete = "incomplete", @@ -76,6 +77,12 @@ export const StepperControls: React.FC = ({ }) => { const { t } = useTranslation("labelDataValidator"); const stepsTotal = stepTitles.length; + const [loading, setLoading] = useState(false); + + const handleSubmission = () => { + setLoading(true); + submit(); + }; return ( @@ -88,16 +95,16 @@ export const StepperControls: React.FC = ({ > {t("stepper.back")} - + /> - @@ -853,6 +877,28 @@ const LabelDataConfirmationPage = () => { ))} + + {/* Notes */} + + + {t("notes.sectionTitle")} + + setComment(e.target.value)} + disabled={confirmed} + /> + { /> + - diff --git a/src/stores/alertStore.ts b/src/stores/alertStore.ts index 628b7e5d..d83a1c14 100644 --- a/src/stores/alertStore.ts +++ b/src/stores/alertStore.ts @@ -10,7 +10,7 @@ interface AlertState { const useAlertStore = create((set) => ({ alert: null, showAlert: (message, type = "info") => { - console.log(`Alert: ${message} | Type: ${type}`); + console.debug(`Alert: ${message} | Type: ${type}`); set({ alert: { message, type } }); }, hideAlert: () => set({ alert: null }), diff --git a/src/stores/fileStore.ts b/src/stores/fileStore.ts index 2db3c8cf..85cc3eac 100644 --- a/src/stores/fileStore.ts +++ b/src/stores/fileStore.ts @@ -13,12 +13,12 @@ const useUploadedFilesStore = create((set) => ({ uploadedFiles: [], addUploadedFile: (file) => { - console.log(`[File Store] Added: ${file.getInfo().path}`); + console.debug(`[File Store] Added: ${file.getInfo().path}`); set((state) => ({ uploadedFiles: [...state.uploadedFiles, file] })); }, removeUploadedFile: (path) => { - console.log(`[File Store] Removed: ${path}`); + console.debug(`[File Store] Removed: ${path}`); set((state) => ({ uploadedFiles: state.uploadedFiles.filter( (file) => file.getInfo().path !== path, @@ -32,7 +32,7 @@ const useUploadedFilesStore = create((set) => ({ }, renameUploadedFile: (path, newName) => { - console.log(`[File Store] Renamed: ${path} -> ${newName}`); + console.debug(`[File Store] Renamed: ${path} -> ${newName}`); set((state) => ({ uploadedFiles: state.uploadedFiles.map((file) => file.getInfo().path === path diff --git a/src/stores/labelDataStore.ts b/src/stores/labelDataStore.ts index 097d06d8..d9d75e2a 100644 --- a/src/stores/labelDataStore.ts +++ b/src/stores/labelDataStore.ts @@ -4,6 +4,7 @@ import { create } from "zustand"; interface LabelDataState { labelData: LabelData | null; setLabelData: (newData: LabelData) => void; + setComment: (comment: string) => void; resetLabelData: () => void; } @@ -11,12 +12,19 @@ const useLabelDataStore = create((set) => ({ labelData: null, setLabelData: (newData) => { - console.log(`[Label Store] Set label data:`, newData); + console.debug(`[Label Store] Set label data:`, newData); set({ labelData: newData }); }, + setComment: (comment) => { + console.debug(`[Label Store] Set comment:`, comment); + set((state) => ({ + labelData: state.labelData ? { ...state.labelData, comment } : null, + })); + }, + resetLabelData: () => { - console.log(`[Label Store] Reset label data`); + console.debug(`[Label Store] Reset label data`); set({ labelData: null }); }, })); diff --git a/src/utils/client/constants.ts b/src/utils/client/constants.ts index 70d87380..275dadb0 100644 --- a/src/utils/client/constants.ts +++ b/src/utils/client/constants.ts @@ -96,7 +96,7 @@ export const VERIFIED_LABEL_DATA: LabelData = { }, ], confirmed: false, - comment: "", + comment: "Compliant with regulations.", }; export const VERIFIED_LABEL_DATA_WITH_ID = { From e706200c4473cc6ef26c351a83bf2f1d048872bb Mon Sep 17 00:00:00 2001 From: "K. Allagbe" Date: Thu, 6 Feb 2025 14:07:32 -0700 Subject: [PATCH 5/6] issue #449: add missing ingredients section in expanded view --- src/app/label-data-confirmation/page.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/app/label-data-confirmation/page.tsx b/src/app/label-data-confirmation/page.tsx index c14a9a5b..1b869d19 100644 --- a/src/app/label-data-confirmation/page.tsx +++ b/src/app/label-data-confirmation/page.tsx @@ -878,6 +878,26 @@ const LabelDataConfirmationPage = () => { ))} + {/* Ingredients */} + + + {t("ingredients.sectionTitle")} + + + + {t("ingredients.nutrients")} + + + + + {/* Notes */} Date: Thu, 6 Feb 2025 17:51:40 -0700 Subject: [PATCH 6/6] issue #449: reduce component duplication in confirmation page --- package-lock.json | 15 - src/app/label-data-confirmation/page.tsx | 1077 ++++++++-------------- 2 files changed, 362 insertions(+), 730 deletions(-) diff --git a/package-lock.json b/package-lock.json index 073b3337..499bfb9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11246,21 +11246,6 @@ "optional": true } } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", - "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/src/app/label-data-confirmation/page.tsx b/src/app/label-data-confirmation/page.tsx index 1b869d19..2426fa8a 100644 --- a/src/app/label-data-confirmation/page.tsx +++ b/src/app/label-data-confirmation/page.tsx @@ -50,7 +50,7 @@ const LabelDataConfirmationPage = () => { const showAlert = useAlertStore((state) => state.showAlert); const [confirmed, setConfirmed] = useState(false); const { t } = useTranslation("confirmationPage"); - const [isRetractedView, setIsRetractedView] = useState(false); + const [isRetractedView, setIsRetractedView] = useState(true); const getAuthHeader = () => { return "Basic " + btoa(`${atob(Cookies.get("token") ?? "")}:`); @@ -171,7 +171,7 @@ const LabelDataConfirmationPage = () => { return ( @@ -179,7 +179,7 @@ const LabelDataConfirmationPage = () => { className="flex flex-col lg:flex-row gap-4 my-4 lg:h-[85vh] lg:min-h-[500px] " data-testid="main-content" > - {!isRetractedView && ( + {isRetractedView && ( { )} { data-testid="retract-button" > - {isRetractedView ? ( + {!isRetractedView ? ( isLgOrBelow ? ( { )} - {!isRetractedView && ( - - - {/* Title */} + + + + {/* Title */} + + {t("pageTitle")} + + + + {/* Label section */} +
+ {/* Base Information */} + - {t("pageTitle")} + {t("baseInformation.sectionTitle")} + + + + + + + {t("baseInformation.tableHeaders.name")} + + + + + {t( + "baseInformation.tableHeaders.registrationNumber", + )} + + + + + {t("baseInformation.tableHeaders.lotNumber")} + + + + + {t("baseInformation.tableHeaders.npk")} + + + + + {t("baseInformation.tableHeaders.weight")} + + + + + {t("baseInformation.tableHeaders.density")} + + + + + {t("baseInformation.tableHeaders.volume")} + + + + + + + + + {labelData?.baseInformation.name.value} + + + + + { + labelData?.baseInformation.registrationNumber + .value + } + + + + + {labelData?.baseInformation.lotNumber.value} + + + + + {labelData?.baseInformation.npk.value} + + + + + + + + + + + + + +
+
- - {/* Base Information */} - - - {t("baseInformation.sectionTitle")} - - - - - - - - {t("baseInformation.tableHeaders.name")} - + {/* Organizations Table */} + + + {t("organizations.sectionTitle")} + + +
+ + + + + {t("organizations.tableHeaders.name")} + + + + + {t("organizations.tableHeaders.address")} + + + + + {t("organizations.tableHeaders.website")} + + + + + {t("organizations.tableHeaders.phoneNumber")} + + + + + + {labelData?.organizations?.map((org, index) => ( + + + {org.name.value} - - - {t( - "baseInformation.tableHeaders.registrationNumber", - )} - + + {org.address.value} - - - {t("baseInformation.tableHeaders.lotNumber")} + + + + {org.website.value} + - - - {t("baseInformation.tableHeaders.npk")} + + + + {org.phoneNumber.value} + - + + ))} + +
+
+
+ + {/* Cautions */} + + + {t("cautions.sectionTitle")} + + + + + {/* Instructions */} + + + {t("instructions.sectionTitle")} + + + + + {/* Guaranteed Analysis */} + + + {t("guaranteedAnalysis.sectionTitle")} + + + {/* Title Section */} + + + {t("guaranteedAnalysis.title")} + + + + + + - {t("baseInformation.tableHeaders.weight")} + {t("guaranteedAnalysis.tableHeaders.english")} - + - {t("baseInformation.tableHeaders.density")} + {t("guaranteedAnalysis.tableHeaders.french")} - + - {t("baseInformation.tableHeaders.volume")} + {t("guaranteedAnalysis.tableHeaders.isMinimal")} - - + + - {labelData?.baseInformation.name.value} + {labelData?.guaranteedAnalysis.titleEn.value} - + - { - labelData?.baseInformation.registrationNumber - .value - } + {labelData?.guaranteedAnalysis.titleFr.value} - + - {labelData?.baseInformation.lotNumber.value} - - - - - {labelData?.baseInformation.npk.value} - - - - - - - - - - - - - -
-
-
- - {/* Organizations Table */} - - - {t("organizations.sectionTitle")} - - - - - - - - {t("organizations.tableHeaders.name")} - - - - - {t("organizations.tableHeaders.address")} - - - - - {t("organizations.tableHeaders.website")} - - - - - {t("organizations.tableHeaders.phoneNumber")} + {labelData?.guaranteedAnalysis.isMinimal.value + ? t("yes") + : t("no")} - - - {labelData?.organizations?.map((org, index) => ( - - - {org.name.value} - - - {org.address.value} - - - - - {org.website.value} - - - - - - - {org.phoneNumber.value} - - - - - ))}
- {/* Cautions */} - - - {t("cautions.sectionTitle")} + {/* Nutrients Section */} + + + {t("guaranteedAnalysis.nutrients")} + - {/* Instructions */} - - - {t("instructions.sectionTitle")} + {/* Ingredients */} + + + {t("ingredients.sectionTitle")} + + + + {t("ingredients.nutrients")} - - - {/* Guaranteed Analysis */} - - - {t("guaranteedAnalysis.sectionTitle")} - - - {/* Title Section */} - - - {t("guaranteedAnalysis.title")} - - - - - - - - {t("guaranteedAnalysis.tableHeaders.english")} - - - - - {t("guaranteedAnalysis.tableHeaders.french")} - - - - - {t("guaranteedAnalysis.tableHeaders.isMinimal")} - - - - - - - - - {labelData?.guaranteedAnalysis.titleEn.value} - - - - - {labelData?.guaranteedAnalysis.titleFr.value} - - - - - {labelData?.guaranteedAnalysis.isMinimal.value - ? t("yes") - : t("no")} - - - - -
-
-
- - {/* Nutrients Section */} - - - {t("guaranteedAnalysis.nutrients")} - - - -
- - {/* Ingredients */} - - - {t("ingredients.sectionTitle")} - - - - {t("ingredients.nutrients")} - - - - - - {/* Notes */} - - - {t("notes.sectionTitle")} - - setComment(e.target.value)} - disabled={confirmed} + data={labelData?.ingredients ?? []} + data-testid="ingredients-nutrients-table" />
- {/* Confirmation Section */} - - {t("confirmationSection.prompt")} - {/* Acknowledgment Checkbox */} - - setConfirmed(event.target.checked)} - disabled={loading} - data-testid="confirmation-checkbox" - /> - } - label={ - - {t("confirmationSection.acknowledgment")} - - } - /> - - - {/* Confirm and Edit Buttons */} - - - - - -
- )} - {isRetractedView && ( - <> - - {/* Title */} + {/* Notes */} + - {t("pageTitle")} + {t("notes.sectionTitle")} + setComment(e.target.value)} + disabled={confirmed} + /> - - {/* Left Column: Base Information */} - - {/* Title */} - - {t("baseInformation.sectionTitle")} - - - - - - - {t("baseInformation.tableHeaders.name")} - - - {labelData?.baseInformation.name.value} - - - - - {t( - "baseInformation.tableHeaders.registrationNumber", - )} - - - { - labelData?.baseInformation.registrationNumber - .value - } - - - - - {t("baseInformation.tableHeaders.lotNumber")} - - - {labelData?.baseInformation.lotNumber.value} - - - - - {t("baseInformation.tableHeaders.npk")} - - - {labelData?.baseInformation.npk.value} - - - - - {t("baseInformation.tableHeaders.weight")} - - - - - - - - {t("baseInformation.tableHeaders.density")} - - - - - - - - {t("baseInformation.tableHeaders.volume")} - - - - - - -
-
- - {/* Cautions */} - - - {t("cautions.sectionTitle")} - - - - - {/* Instructions */} - - - {t("instructions.sectionTitle")} - - - - - {/* Guaranteed Analysis */} - - - {t("guaranteedAnalysis.sectionTitle")} - - - {/* Nutrients Section with Clear Headers */} - - - {t("guaranteedAnalysis.nutrients")} - - - - - - - - {t("bilingualTable.tableHeaders.english")} - - - - - {t("bilingualTable.tableHeaders.french")} - - - - - {t("bilingualTable.tableHeaders.value")} - - - - - {t("bilingualTable.tableHeaders.unit")} - - - - - - {labelData?.guaranteedAnalysis.nutrients.map( - (nutrient, index) => ( - - - {nutrient.en} - - - {nutrient.fr} - - - {nutrient.value} - - - {nutrient.unit} - - - ), - )} - -
-
-
-
-
- - {/* Right Column: Organizations */} - - {labelData?.organizations?.map((org, index) => ( - - - {t("organizations.sectionTitle") + " " + (index + 1)} - - - - - - - {t("organizations.tableHeaders.name")} - - {org.name.value} - - - - {t("organizations.tableHeaders.address")} - - {org.address.value} - - - - {t("organizations.tableHeaders.website")} - - - - {org.website.value} - - - - - - {t("organizations.tableHeaders.phoneNumber")} - - - - {org.phoneNumber.value} - - - - -
-
-
- ))} - - {/* Ingredients */} - - - {t("ingredients.sectionTitle")} - - - - {t("ingredients.nutrients")} - - - - - - {/* Notes */} - - - {t("notes.sectionTitle")} - - setComment(e.target.value)} - disabled={confirmed} +
+ + {/* Confirmation Section */} + + {t("confirmationSection.prompt")} + {/* Acknowledgment Checkbox */} + + setConfirmed(event.target.checked)} + disabled={loading} + data-testid="confirmation-checkbox" /> - -
-
- - + {t("confirmationSection.acknowledgment")} +
+ } + /> + + + {/* Confirm and Edit Buttons */} + + - - -
+ {t("confirmationSection.editButton")} + + - - )} + +