Skip to content

Commit

Permalink
Place front end logic for saving manifest to server and e-Manifest in…
Browse files Browse the repository at this point in the history
… a custom hook, useSaveManifest. This hook wraps around our RTK query endpoints/hooks. Depending on the manifest saved, it initiates the appropriate action to POST/PUT to the haztrak server or saving to e-Manifest
  • Loading branch information
dpgraham4401 committed Jan 9, 2024
1 parent bb8ae0b commit 923c895
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 118 deletions.
16 changes: 8 additions & 8 deletions client/src/components/Manifest/Actions/ManifestEditBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ interface ManifestEditBtnProps extends ButtonProps {}
export function ManifestEditBtn({ children, ...props }: ManifestEditBtnProps) {
const navigate = useNavigate();
const { trackingNumber, viewingAsSiteId } = useContext(ManifestContext);
const [readOnly] = useReadOnly();
const [readOnly, setReadOnly] = useReadOnly();
if (!readOnly) return <></>;

const handleClick = () => {
setReadOnly(false);
navigate(`/site/${viewingAsSiteId}/manifest/${trackingNumber}/edit`);
};

return (
<HtButton
{...props}
variant="info"
type="button"
name="edit"
onClick={() => navigate(`/site/${viewingAsSiteId}/manifest/${trackingNumber}/edit`)}
>
<HtButton {...props} variant="info" type="button" name="edit" onClick={handleClick}>
<span>Edit </span>
<FontAwesomeIcon icon={faPenToSquare} />
</HtButton>
Expand Down
60 changes: 35 additions & 25 deletions client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Manifest, ManifestStatus } from 'components/Manifest/manifestSchema';
import { HtForm, InfoIconTooltip } from 'components/UI';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import { useGetProfileQuery } from 'store';
import { manifest } from 'services';
import Select, { SingleValue } from 'react-select';
Expand Down Expand Up @@ -51,30 +51,40 @@ export function ManifestStatusSelect({ readOnly, isDraft }: ManifestStatusFieldP
<InfoIconTooltip message={'Once set to scheduled, this field is managed by EPA'} />
)}
</HtForm.Label>
<Select
id="status"
isDisabled={readOnly || !isDraft}
data-testid="manifestStatus"
aria-label="manifestStatus"
{...manifestForm.register('status')}
value={selectedStatus}
isLoading={profileLoading || !profile}
classNames={{
control: () => 'form-select py-0 rounded-3',
}}
onChange={(option: SingleValue<StatusOption>) => {
if (option) setStatus(option.value);
}}
options={statusOptions}
filterOption={(option) =>
// Hide options that are managed by EPA
availableStatuses.includes(option.value as ManifestStatus) || option.value === 'Scheduled'
}
isOptionDisabled={(option) =>
// Disable the 'Scheduled' option if it's not available
option.value === 'Scheduled' && !availableStatuses.includes('Scheduled')
}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
<Controller
name="status"
control={manifestForm.control}
render={({ field }) => (
<Select
{...field}
id="status"
aria-label="status"
value={selectedStatus}
options={statusOptions}
isDisabled={readOnly || !isDraft}
data-testid="manifestStatus"
isLoading={profileLoading || !profile}
onChange={(option: SingleValue<StatusOption>) => {
if (option) {
setStatus(option.value);
field.onChange(option.value);
}
}}
filterOption={(option) =>
// Hide options that are managed by EPA
availableStatuses.includes(option.value as ManifestStatus) ||
option.value === 'Scheduled'
}
isOptionDisabled={(option) =>
// Disable the 'Scheduled' option if it's not available
option.value === 'Scheduled' && !availableStatuses.includes('Scheduled')
}
classNames={{
control: () => 'form-select py-0 rounded-3',
}}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
/>
)}
/>
</HtForm.Group>
);
Expand Down
68 changes: 42 additions & 26 deletions client/src/components/Manifest/GeneralInfo/ManifestTypeSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Manifest, SubmissionType } from 'components/Manifest/manifestSchema';
import { HtForm } from 'components/UI';
import React from 'react';
import Select from 'react-select';
import { useFormContext } from 'react-hook-form';
import React, { useState } from 'react';
import Select, { SingleValue } from 'react-select';
import { Controller, useFormContext } from 'react-hook-form';

const submissionTypeOptions: Array<{ value: SubmissionType; label: string }> = [
interface SubmissionTypeOption {
value: SubmissionType;
label: string;
}

const submissionTypeOptions: Array<SubmissionTypeOption> = [
{ value: 'Hybrid', label: 'Hybrid' },
{ value: 'FullElectronic', label: 'Electronic' },
{ value: 'DataImage5Copy', label: 'Data + Image' },
{ value: 'Image', label: 'Image Only' },
];

const DEFAULT_SUBMISSION_TYPE = submissionTypeOptions[0];

/** uniform hazardous waste manifest type field. */
export function ManifestTypeSelect({
readOnly,
Expand All @@ -23,31 +26,44 @@ export function ManifestTypeSelect({
}) {
const manifestForm = useFormContext<Manifest>();
const generatorCanESign = manifestForm.getValues('generator.canEsign');
const [submissionType, setSubmissionType] = useState<SubmissionType>(
manifestForm.getValues('submissionType') || 'Hybrid'
);
const selectedType = submissionTypeOptions.filter((option) => option.value === submissionType);
return (
<HtForm.Group>
<HtForm.Label htmlFor="submissionType" className="mb-0">
Type
</HtForm.Label>
<Select
id="submissionType"
isDisabled={readOnly || !isDraft}
aria-label="submissionType"
{...manifestForm.register('submissionType')}
options={submissionTypeOptions}
getOptionValue={(option) => option.value}
defaultValue={DEFAULT_SUBMISSION_TYPE}
classNames={{
control: () => 'form-select py-0 rounded-3',
}}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
onChange={(option) => {
if (option) manifestForm.setValue('submissionType', option.value);
}}
filterOption={(option) =>
option.label.toLowerCase().includes('electronic') ||
option.label.toLowerCase().includes('hybrid')
}
isOptionDisabled={(option) => option.value === 'FullElectronic' && !generatorCanESign}
<Controller
control={manifestForm.control}
name={'submissionType'}
render={({ field }) => (
<Select
{...field}
id="submissionType"
aria-label="submission type"
isDisabled={readOnly || !isDraft}
value={selectedType}
options={submissionTypeOptions}
defaultValue={submissionTypeOptions[0]}
onChange={(option: SingleValue<SubmissionTypeOption>) => {
if (option) {
setSubmissionType(option.value);
field.onChange(option.value);
}
}}
filterOption={(option) =>
option.label.toLowerCase().includes('electronic') ||
option.label.toLowerCase().includes('hybrid')
}
isOptionDisabled={(option) => option.value === 'FullElectronic' && !generatorCanESign}
classNames={{
control: () => 'form-select py-0 rounded-3',
}}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
/>
)}
/>
</HtForm.Group>
);
Expand Down
71 changes: 21 additions & 50 deletions client/src/components/Manifest/ManifestForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,13 @@ import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema';
import { WasteLineSection } from 'components/Manifest/WasteLine/WasteLineSection';
import { HtCard, HtForm } from 'components/UI';
import { useUserSiteIds } from 'hooks';
import { useManifestStatus, useReadOnly } from 'hooks/manifest';
import { useManifestStatus, useReadOnly, useSaveManifest } from 'hooks/manifest';
import React, { createContext, useEffect, useState } from 'react';
import { Container, Stack } from 'react-bootstrap';
import { FormProvider, SubmitHandler, useFieldArray, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { manifest } from 'services';
import {
useCreateManifestMutation,
useSaveEManifestMutation,
useUpdateManifestMutation,
} from 'store';
import { HandlerSearchModal } from './Handler';
import { Manifest, manifestSchema, SiteType } from './manifestSchema';
import { QuickerSignData, QuickerSignModal } from './QuickerSign';
Expand All @@ -36,7 +31,7 @@ const defaultValues: Manifest = {
transporters: [],
wastes: [],
status: 'NotAssigned',
submissionType: 'FullElectronic',
submissionType: 'Hybrid',
};

export interface ManifestContextType {
Expand Down Expand Up @@ -100,16 +95,14 @@ export function ManifestForm({

// State related to inter-system communications with EPA's RCRAInfo system
const [showSpinner, setShowSpinner] = useState<boolean>(false);
const toggleShowSpinner = () => setShowSpinner(!showSpinner);
const [taskId, setTaskId] = useState<string | undefined>(undefined);
const [createManifest, { data: createData, error: createError, isLoading: createIsLoading }] =
useCreateManifestMutation();
const [
saveEmanifest,
{ data: eManifestResult, error: eManifestError, isLoading: eManifestIsLoading },
] = useSaveEManifestMutation();
const [updateManifest, { data: updateResults, error: updateError, isLoading: updateIsLoading }] =
useUpdateManifestMutation();
const {
isLoading: saveManifestIsLoading,
saveManifest,
data: saveManifestResult,
error: saveManifestError,
taskId: saveEmanifestTaskId,
} = useSaveManifest();

// React-Hook-Form component state and methods
const manifestForm = useForm<Manifest>({
Expand All @@ -118,53 +111,31 @@ export function ManifestForm({
});

useEffect(() => {
if (createData) {
if ('manifestTrackingNumber' in createData) {
navigate(`/manifest/${createData.manifestTrackingNumber}/view`);
if (saveManifestResult) {
if ('manifestTrackingNumber' in saveManifestResult) {
navigate(
`/site/${manifestingSiteID}/manifest/${saveManifestResult.manifestTrackingNumber}/view`
);
}
}
if (createIsLoading) {
if (saveManifestIsLoading) {
setShowSpinner(true);
}
if (createError) {
if (saveManifestError) {
toast.error('Error creating manifest');
setShowSpinner(false);
}
}, [createData, createIsLoading, createError]);
}, [saveManifestResult, saveManifestIsLoading, saveManifestError]);

useEffect(() => {
if (updateResults) {
if ('manifestTrackingNumber' in updateResults) {
navigate(`/manifest/${updateResults.manifestTrackingNumber}/view`);
}
}
if (updateIsLoading) {
if (saveEmanifestTaskId) {
setTaskId(saveEmanifestTaskId);
setShowSpinner(true);
}
if (updateError) {
console.error(updateError);
toast.error('Error Updating manifest');
setShowSpinner(false);
}
}, [updateResults, updateError, updateIsLoading]);

useEffect(() => {
if (eManifestResult) {
setTaskId(eManifestResult.taskId);
toggleShowSpinner();
}
}, [eManifestResult, eManifestIsLoading, eManifestError]);
}, [saveEmanifestTaskId]);

const onSubmit: SubmitHandler<Manifest> = (data: Manifest) => {
if (data.status === 'NotAssigned') {
if (data.manifestTrackingNumber?.endsWith('DFT')) {
updateManifest({ mtn: data.manifestTrackingNumber, manifest: data });
} else {
createManifest(data);
}
} else {
saveEmanifest(data);
}
saveManifest(data);
};

// Generator component state and methods
Expand Down
8 changes: 7 additions & 1 deletion client/src/features/NewManifest/NewManifest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,11 @@ export function NewManifest() {
? { designatedFacility: manifestingSite }
: {};

return <ManifestForm manifestData={newManifestData} readOnly={false} />;
return (
<ManifestForm
manifestData={newManifestData}
manifestingSiteID={manifestingSite.epaSiteId}
readOnly={false}
/>
);
}
1 change: 1 addition & 0 deletions client/src/hooks/manifest/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { useManifestStatus } from './useManifestStatus/useManifestStatus';
export { useReadOnly } from './useReadOnly/useReadOnly';
export { useSaveManifest } from './useSaveManifest/useSaveManifest';
69 changes: 69 additions & 0 deletions client/src/hooks/manifest/useSaveManifest/useSaveManifest.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import '@testing-library/jest-dom';
import { cleanup, waitFor } from '@testing-library/react';
import React from 'react';
import { renderWithProviders, screen } from 'test-utils';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { useSaveManifest } from './useSaveManifest';
import { Manifest } from 'components/Manifest';
import userEvent from '@testing-library/user-event';
import { createMockManifest } from 'test-utils/fixtures';
import { setupServer } from 'msw/node';
import { manifestMocks } from 'test-utils/mock';

const TestComponent = ({ manifest }: { manifest?: Manifest }) => {
const { data, isLoading, error, taskId, saveManifest } = useSaveManifest();
return (
<>
<p>loading: {isLoading ? 'yes' : 'no'}</p>
<p>error: {error ? 'yes' : 'no'}</p>
<p>task: {taskId ? taskId : 'empty'}</p>
<p>data: {data ? 'defined' : 'undefined'}</p>
<p>mtn: {data?.manifestTrackingNumber ?? 'undefined'}</p>
<button onClick={() => saveManifest(manifest)}>save</button>
</>
);
};

const server = setupServer(...manifestMocks);
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => cleanup());

describe('useSaveManifest', () => {
it('isLoading is false, error is null, and taskId is null by default ', () => {
renderWithProviders(<TestComponent />);
expect(screen.getByText(/^loading: no/i)).toBeInTheDocument();
expect(screen.getByText(/^error: no/i)).toBeInTheDocument();
expect(screen.getByText(/^task: empty/i)).toBeInTheDocument();
});
it('request are made to create a draft manifest', async () => {
renderWithProviders(
<TestComponent
manifest={createMockManifest({
manifestTrackingNumber: undefined,
status: 'NotAssigned',
})}
/>
);
await userEvent.click(screen.getByText(/^save$/i));
await waitFor(() => expect(screen.getByText(/^loading: no/i)).toBeInTheDocument());
expect(screen.getByText(/^data: defined/i)).toBeInTheDocument();
expect(screen.getByText(/^mtn: [0-9]{9}DFT/i)).toBeInTheDocument();
});
it('request to update a draft manifest if MTN already exists', async () => {
const existingMTN = '123456789DFT';
renderWithProviders(
<TestComponent
manifest={createMockManifest({
manifestTrackingNumber: existingMTN,
status: 'NotAssigned',
})}
/>
);
expect(screen.getByText(/^loading: no/i)).toBeInTheDocument();
await userEvent.click(screen.getByText(/^save$/i));
await waitFor(() => expect(screen.getByText(/^loading: no/i)).toBeInTheDocument());
expect(screen.getByText(/^data: defined/i)).toBeInTheDocument();
expect(screen.getByText(`mtn: ${existingMTN}`)).toBeInTheDocument();
});
});
Loading

0 comments on commit 923c895

Please sign in to comment.