From 811eb73d1a3e433bec2b47cbc91ea8c66cf28057 Mon Sep 17 00:00:00 2001 From: aleckvincent Date: Mon, 9 Sep 2024 13:30:00 +0200 Subject: [PATCH 01/20] bugfix 331 - edit pen icon on long description --- .../timeline/item/timeline-item-status.test.tsx | 17 +++++++++++++++++ .../timeline/item/timeline-item-status.tsx | 13 ++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.test.tsx b/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.test.tsx index a71cd37b..8fa5fa6b 100644 --- a/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.test.tsx +++ b/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.test.tsx @@ -80,4 +80,21 @@ describe('ActionStatus', () => { render() expect(screen.queryByText('Indisponibilité - Technique - fin')).toBeInTheDocument() }) + test('should truncate observations longer than 35 characters and add ellipsis', () => { + const longObservation = 'This is a very long observation that exceeds thirty-five characters.' + + const mock = { ...actionMock, data: { ...actionMock.data, observations: longObservation } } + render() + + const expectedTruncatedText = longObservation.slice(0, 35) + '...' + expect(screen.getByText(new RegExp(expectedTruncatedText))).toBeInTheDocument() + }) + it('should display observations as is if shorter than 35 characters', () => { + const shortObservation = 'This is short.' + + const mock = { ...actionMock, data: { ...actionMock.data, observations: shortObservation } } + render() + + expect(screen.getByText('- ' + shortObservation)).toBeInTheDocument() + }) }) diff --git a/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.tsx b/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.tsx index 9c5a7a59..b3d70f29 100644 --- a/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.tsx +++ b/frontend/src/features/pam/mission/components/elements/timeline/item/timeline-item-status.tsx @@ -15,7 +15,7 @@ const ActionStatus: FC = ({ action, previousActionWith const prevActionData = previousActionWithSameType?.data as unknown as NavActionStatus return ( - + @@ -27,16 +27,15 @@ const ActionStatus: FC = ({ action, previousActionWith weight="normal" color={isSelected ? THEME.color.charcoal : THEME.color.slateGray} decoration={isSelected ? 'underline' : 'normal'} - style={{ - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis' - }} > {`${mapStatusToText(actionData?.status)} - début${ !!actionData?.reason ? ' - ' + statusReasonToHumanString(actionData?.reason) : '' }`} - {!!actionData?.observations ? ' - ' + actionData?.observations : ''} + {!!actionData?.observations + ? ' - ' + (actionData?.observations.length > 35 + ? actionData?.observations.slice(0, 35) + '...' + : actionData?.observations) + : ''} From 9c797ce8dd707c62c010cc77783a161032de354e Mon Sep 17 00:00:00 2001 From: aleckvincent Date: Mon, 9 Sep 2024 08:47:04 +0200 Subject: [PATCH 02/20] bugfix/329 add background hover on whole MissionItem and fix AEM report download button alignment --- .../components/elements/mission-item.tsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/frontend/src/features/pam/mission/components/elements/mission-item.tsx b/frontend/src/features/pam/mission/components/elements/mission-item.tsx index 28084184..6e36fd80 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-item.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-item.tsx @@ -92,7 +92,7 @@ const MissionItem: React.FC = ({ mission, prefetchMission }) = } const onItemMouseOver = () => { - const isCompleteForStats = mission?.completenessForStats?.status === CompletenessForStatsStatusEnum.COMPLETE + const isCompleteForStats = true //mission?.completenessForStats?.status === CompletenessForStatsStatusEnum.COMPLETE if (isCompleteForStats) { setExportationCanBeDisplayed(true) @@ -149,40 +149,40 @@ const MissionItem: React.FC = ({ mission, prefetchMission }) = + {exportationCanBeDisplayed && ( + + + + + + + + + + + + + )} - {exportationCanBeDisplayed && ( - - - - - - - - - - - - - - )} + ) From ff6e5ec2b180d0b2206cd0548d69d76c9ca7bab5 Mon Sep 17 00:00:00 2001 From: aleckvincent Date: Mon, 9 Sep 2024 08:48:41 +0200 Subject: [PATCH 03/20] remove true on isCompleteForStats --- .../features/pam/mission/components/elements/mission-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/pam/mission/components/elements/mission-item.tsx b/frontend/src/features/pam/mission/components/elements/mission-item.tsx index 6e36fd80..06be6444 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-item.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-item.tsx @@ -92,7 +92,7 @@ const MissionItem: React.FC = ({ mission, prefetchMission }) = } const onItemMouseOver = () => { - const isCompleteForStats = true //mission?.completenessForStats?.status === CompletenessForStatsStatusEnum.COMPLETE + const isCompleteForStats = mission?.completenessForStats?.status === CompletenessForStatsStatusEnum.COMPLETE if (isCompleteForStats) { setExportationCanBeDisplayed(true) From 4080f1e814c839a83bc6cba7a8bd837eca732bf3 Mon Sep 17 00:00:00 2001 From: aleckvincent Date: Mon, 9 Sep 2024 11:32:43 +0200 Subject: [PATCH 04/20] add test for bugfix hover missionItem --- .../components/elements/mission-item.test.tsx | 16 ++++++++++++++++ .../mission/components/elements/mission-item.tsx | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx b/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx index c602e556..a758cfb3 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx @@ -8,6 +8,8 @@ import { fireEvent } from '@testing-library/react' import * as useIsMissionCompleteForStatsModule from '@features/pam/mission/hooks/use-is-mission-complete-for-stats' import * as useAEMModule from '@features/pam/mission/hooks/export/use-lazy-mission-aem-export' import * as useExportModule from '@features/pam/mission/hooks/export/use-lazy-mission-export' +import { hexToRgb } from '@common/utils/colors.ts' +import { THEME } from '@mtes-mct/monitor-ui' const mission = { id: 3, @@ -110,4 +112,18 @@ describe('Mission Item component', () => { expect(exportLazyAEMMock).toHaveBeenCalled() }) + + test('should have a background color blueGray25 on ListItemHover when mouseOver', () => { + vi.spyOn(useIsMissionCompleteForStatsModule, 'default').mockReturnValue({ data: true, loading: false, error: null }) + vi.spyOn(useIsMissionCompleteForStatsModule, 'default').mockReturnValue({ data: true, loading: false, error: null }) + + const { container } = render() + const missionItemElement = container.firstChild + + fireEvent.mouseOver(missionItemElement) + const listItemWithHover = screen.getByTestId('list-item-with-hover') + console.log(listItemWithHover) + expect(getComputedStyle(listItemWithHover).backgroundColor).toBe(hexToRgb(THEME.color.blueGray25)) + + }) }) diff --git a/frontend/src/features/pam/mission/components/elements/mission-item.tsx b/frontend/src/features/pam/mission/components/elements/mission-item.tsx index 06be6444..deb5d1b7 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-item.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-item.tsx @@ -106,7 +106,7 @@ const MissionItem: React.FC = ({ mission, prefetchMission }) = return ( - + Date: Mon, 9 Sep 2024 11:37:19 +0200 Subject: [PATCH 05/20] remove useless console.log --- .../pam/mission/components/elements/mission-item.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx b/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx index a758cfb3..beb1569b 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-item.test.tsx @@ -122,7 +122,6 @@ describe('Mission Item component', () => { fireEvent.mouseOver(missionItemElement) const listItemWithHover = screen.getByTestId('list-item-with-hover') - console.log(listItemWithHover) expect(getComputedStyle(listItemWithHover).backgroundColor).toBe(hexToRgb(THEME.color.blueGray25)) }) From 310940d775dbe89e4484c380d52e055787101b0c Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 4 Sep 2024 15:27:25 +0200 Subject: [PATCH 06/20] feature(action): dropdown add action - Create a common component action-dropdown with 2 menu items, main and sub - on click, show sub menu item - remove old component - test it --- .../components/ui/action-dropdown.test.tsx | 34 +++++++++ .../common/components/ui/action-dropdown.tsx | 71 +++++++++++++++++++ .../action-selection-dropdown.test.tsx | 19 ----- .../actions/action-selection-dropdown.tsx | 53 -------------- .../components/elements/mission-content.tsx | 30 ++++---- 5 files changed, 120 insertions(+), 87 deletions(-) create mode 100644 frontend/src/features/common/components/ui/action-dropdown.test.tsx create mode 100644 frontend/src/features/common/components/ui/action-dropdown.tsx delete mode 100644 frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.test.tsx delete mode 100644 frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.tsx diff --git a/frontend/src/features/common/components/ui/action-dropdown.test.tsx b/frontend/src/features/common/components/ui/action-dropdown.test.tsx new file mode 100644 index 00000000..d4cceff2 --- /dev/null +++ b/frontend/src/features/common/components/ui/action-dropdown.test.tsx @@ -0,0 +1,34 @@ +import { vi } from 'vitest' +import { act, fireEvent, render, screen } from '../../../../test-utils' +import ActionDropdown from './action-dropdown' + +const onSelect = vi.fn() + +describe('ActionDropdown', () => { + it('should render main items menu', () => { + render() + + expect(screen.getByText('Ajouter des contrôles')).toBeInTheDocument() + expect(screen.getByText('Ajouter une note libre')).toBeInTheDocument() + expect(screen.getByText('Ajouter une assistance / sauvetage')).toBeInTheDocument() + expect(screen.getByText('Ajouter une autre activité de mission')).toBeInTheDocument() + }) + + it('should render main items and sub items', async () => { + const wrapper = render() + const item = wrapper.getByText('Ajouter une autre activité de mission') + act(() => { + fireEvent.click(item) + }) + expect(screen.getByText('Ajouter des contrôles')).toBeInTheDocument() + expect(screen.getByText('Ajouter une note libre')).toBeInTheDocument() + expect(screen.getByText('Ajouter une assistance / sauvetage')).toBeInTheDocument() + expect(screen.getByText('Sécu de manifestation nautique')).toBeInTheDocument() + expect(screen.getByText('Permanence Vigimer')).toBeInTheDocument() + expect(screen.getByText('Opération de lutte anti-pollution')).toBeInTheDocument() + expect(screen.getByText('Permanence BAAEM')).toBeInTheDocument() + expect(screen.getByText("Maintien de l'ordre public")).toBeInTheDocument() + expect(screen.getByText('Représentation')).toBeInTheDocument() + expect(screen.getByText("Lutte contre l'immigration illégale")).toBeInTheDocument() + }) +}) diff --git a/frontend/src/features/common/components/ui/action-dropdown.tsx b/frontend/src/features/common/components/ui/action-dropdown.tsx new file mode 100644 index 00000000..1fae84d8 --- /dev/null +++ b/frontend/src/features/common/components/ui/action-dropdown.tsx @@ -0,0 +1,71 @@ +import { ActionTypeEnum } from '@common/types/env-mission-types.ts' +import { Dropdown, Icon, IconProps, THEME } from '@mtes-mct/monitor-ui' +import { FC, useState } from 'react' +import styled from 'styled-components' + +type DropdownSubItem = { type: ActionTypeEnum; label: string } + +type DropdownItem = DropdownSubItem & { icon: React.FunctionComponent } + +const DROPDOWN_ITEMS: DropdownItem[] = [ + { icon: Icon.ControlUnit, type: ActionTypeEnum.CONTROL, label: 'Ajouter des contrôles' }, + { icon: Icon.Note, type: ActionTypeEnum.NOTE, label: 'Ajouter une note libre' }, + { icon: Icon.Rescue, type: ActionTypeEnum.RESCUE, label: 'Ajouter une assistance / sauvetage' }, + { icon: Icon.More, type: ActionTypeEnum.OTHER, label: 'Ajouter une autre activité de mission' } +] + +const DROPDOWN_SUB_ITEMS: DropdownSubItem[] = [ + { type: ActionTypeEnum.NAUTICAL_EVENT, label: 'Sécu de manifestation nautique' }, + { type: ActionTypeEnum.BAAEM_PERMANENCE, label: 'Permanence BAAEM' }, + { type: ActionTypeEnum.VIGIMER, label: 'Permanence Vigimer' }, + { type: ActionTypeEnum.ANTI_POLLUTION, label: 'Opération de lutte anti-pollution' }, + { type: ActionTypeEnum.ILLEGAL_IMMIGRATION, label: `Lutte contre l'immigration illégale` }, + { type: ActionTypeEnum.PUBLIC_ORDER, label: `Maintien de l'ordre public` }, + { type: ActionTypeEnum.REPRESENTATION, label: 'Représentation' } +] + +const DropdownSubItemStyled = styled(Dropdown.Item)(({ theme }) => ({ + color: theme.color.cultured, + backgroundColor: THEME.color.blueYonder, + ':hover': { + color: THEME.color.charcoal, + backgroundColor: THEME.color.blueYonder25 + } +})) + +interface ActionDropdownProps { + onSelect: (key: ActionTypeEnum) => void +} + +const ActionDropdown: FC = ({ onSelect }) => { + const [showSubItem, setShowSubItem] = useState(false) + const handleSelect = (eventKey: ActionTypeEnum, event: React.SyntheticEvent) => { + if (eventKey === ActionTypeEnum.OTHER) { + event.stopPropagation() + setShowSubItem(!showSubItem) + return + } + onSelect(eventKey) + setShowSubItem(false) + } + + const handleClose = () => setShowSubItem(false) + + return ( + + {DROPDOWN_ITEMS.map(item => ( + + {item.label} + + ))} + {showSubItem && + DROPDOWN_SUB_ITEMS.map(item => ( + + {item.label} + + ))} + + ) +} + +export default ActionDropdown diff --git a/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.test.tsx b/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.test.tsx deleted file mode 100644 index c83aaff6..00000000 --- a/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { render, screen } from '../../../../../../test-utils.tsx' -import ActionSelectionDropdown from './action-selection-dropdown.tsx' - -describe('ActionSelectionDropdown', () => { - test('renders control selection options', () => { - render() - - expect(screen.getByText('Ajouter des contrôles')).toBeInTheDocument() - expect(screen.getByText('Ajouter une note libre')).toBeInTheDocument() - expect(screen.getByText('Ajouter une assistance / sauvetage')).toBeInTheDocument() - expect(screen.getByText('Sécu de manifestation nautique')).toBeInTheDocument() - expect(screen.getByText('Permanence Vigimer')).toBeInTheDocument() - expect(screen.getByText('Opération de lutte anti-pollution')).toBeInTheDocument() - expect(screen.getByText('Permanence BAAEM')).toBeInTheDocument() - expect(screen.getByText("Maintien de l'ordre public")).toBeInTheDocument() - expect(screen.getByText('Représentation')).toBeInTheDocument() - expect(screen.getByText("Lutte contre l'immigration illégale")).toBeInTheDocument() - }) -}) diff --git a/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.tsx b/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.tsx deleted file mode 100644 index 01a8d565..00000000 --- a/frontend/src/features/pam/mission/components/elements/actions/action-selection-dropdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { FC } from 'react' -import { Dropdown, Icon } from '@mtes-mct/monitor-ui' -import { Dropdown as RSuiteDropdown } from 'rsuite' -import { ActionTypeEnum } from '@common/types/env-mission-types.ts' -import MoreIcon from '@rsuite/icons/More' - -interface ActionSelectionDropdownProps { - onSelect: (key: ActionTypeEnum) => void -} - -const ActionSelectionDropdown: FC = ({ onSelect }) => { - return ( - - - Ajouter des contrôles - - - Ajouter une note libre - - - Ajouter une assistance / sauvetage - - - {/*} active={false} disabled={true}>*/} - {/* Ajouter une autre activité de mission*/} - {/**/} - } eventKey={ActionTypeEnum.NAUTICAL_EVENT}> - Sécu de manifestation nautique - - } eventKey={ActionTypeEnum.VIGIMER}> - Permanence Vigimer - - } eventKey={ActionTypeEnum.ANTI_POLLUTION}> - Opération de lutte anti-pollution - - } eventKey={ActionTypeEnum.BAAEM_PERMANENCE}> - Permanence BAAEM - - } eventKey={ActionTypeEnum.PUBLIC_ORDER}> - Maintien de l'ordre public - - } eventKey={ActionTypeEnum.REPRESENTATION}> - Représentation - - } eventKey={ActionTypeEnum.ILLEGAL_IMMIGRATION}> - Lutte contre l'immigration illégale - - {/**/} - - ) -} - -export default ActionSelectionDropdown diff --git a/frontend/src/features/pam/mission/components/elements/mission-content.tsx b/frontend/src/features/pam/mission/components/elements/mission-content.tsx index 0bc0b711..761a9396 100644 --- a/frontend/src/features/pam/mission/components/elements/mission-content.tsx +++ b/frontend/src/features/pam/mission/components/elements/mission-content.tsx @@ -1,31 +1,31 @@ +import ActionDropdown from '@common/components/ui/action-dropdown.tsx' +import { Action, ActionStatusType } from '@common/types/action-types.ts' +import { ActionTypeEnum } from '@common/types/env-mission-types.ts' +import { Mission, VesselTypeEnum } from '@common/types/mission-types.ts' +import { formatDateForServers, toLocalISOString } from '@common/utils/dates.ts' import { Accent, Button, Dialog, THEME } from '@mtes-mct/monitor-ui' import find from 'lodash/find' import React, { useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { Divider, FlexboxGrid, Stack } from 'rsuite' -import { Action, ActionStatusType } from '@common/types/action-types.ts' -import { ActionTypeEnum } from '@common/types/env-mission-types.ts' -import { Mission, VesselTypeEnum } from '@common/types/mission-types.ts' import Text from '../../../../common/components/ui/text.tsx' -import { formatDateForServers, toLocalISOString } from '@common/utils/dates.ts' -import { getComponentForAction } from './actions/action-mapping.ts' -import ActionSelectionDropdown from './actions/action-selection-dropdown.tsx' -import useAddOrUpdateControl from '../../hooks/use-add-update-action-control.tsx' -import ControlSelection from './controls/control-selection.tsx' -import MissionRecognizedVessel from './general-info/mission-recognized-vessel.tsx' -import MissionObservationsUnit from './mission-observations-unit.tsx' -import useAddOrUpdateNote from '../../hooks/use-add-update-note.tsx' import useAddAntiPollution from '../../hooks/anti-pollution/use-add-anti-pollution.tsx' import useAddOrUpdateBAAEMPermanence from '../../hooks/baaem/use-add-baaem-permanence.tsx' import useAddIllegalImmigration from '../../hooks/illegal-immigration/use-add-illegal-immigration.tsx' import useAddNauticalEvent from '../../hooks/nautical-event/use-add-nautical-event.tsx' import useAddOrUpdatePublicOrder from '../../hooks/public-order/use-add-public-order.tsx' import useAddRepresentation from '../../hooks/representation/use-add-representation.tsx' -import useAddVigimer from '../../hooks/vigimer/use-add-vigimer.tsx' -import MissionGeneralInfoPanel from './panel-general-info.tsx' import useAddOrUpdateRescue from '../../hooks/rescues/use-add-update-rescue.tsx' -import StatusSelectionDropdown from '../ui/status-selection-dropdown.tsx' +import useAddOrUpdateControl from '../../hooks/use-add-update-action-control.tsx' +import useAddOrUpdateNote from '../../hooks/use-add-update-note.tsx' import useAddOrUpdateStatus from '../../hooks/use-add-update-status.tsx' +import useAddVigimer from '../../hooks/vigimer/use-add-vigimer.tsx' +import StatusSelectionDropdown from '../ui/status-selection-dropdown.tsx' +import { getComponentForAction } from './actions/action-mapping.ts' +import ControlSelection from './controls/control-selection.tsx' +import MissionRecognizedVessel from './general-info/mission-recognized-vessel.tsx' +import MissionObservationsUnit from './mission-observations-unit.tsx' +import MissionGeneralInfoPanel from './panel-general-info.tsx' import MissionTimeline from './timeline/timeline.tsx' export interface MissionProps { @@ -249,7 +249,7 @@ const MissionContent: React.FC = ({ mission }) => { - + From cfbc7d817bfe996f1780c23c98519c086283c5cc Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 22 Aug 2024 13:07:10 +0200 Subject: [PATCH 07/20] feat(control-administrative): delay form mutation for 5 sec - use FormikEffect and Formik component - Add timer to delay mutation - Test eveything --- .../control-administrative-form.test.tsx | 345 ++++++++++++++++++ .../controls/control-administrative-form.tsx | 243 ++++++------ .../components/ui/control-title-checkbox.tsx | 8 +- 3 files changed, 476 insertions(+), 120 deletions(-) create mode 100644 frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.test.tsx diff --git a/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.test.tsx b/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.test.tsx new file mode 100644 index 00000000..2bad49e8 --- /dev/null +++ b/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.test.tsx @@ -0,0 +1,345 @@ +import { MockedProvider, MockedResponse } from '@apollo/client/testing' +import { render, screen } from '@testing-library/react' +import { BrowserRouter, Params } from 'react-router-dom' +import { vi } from 'vitest' +import { fireEvent, waitFor } from '../../../test-utils' +import { ActionAntiPollution } from '../../../types/action-types.ts' +import { ControlAdministrative, ControlResult } from '../../../types/control-types.ts' +import UIThemeWrapper from '../../../ui/ui-theme-wrapper.tsx' +import { GET_ACTION_BY_ID } from '../actions/use-action-by-id.tsx' +import { GET_MISSION_TIMELINE } from '../timeline/use-mission-timeline.tsx' +import ControlAdministrativeForm from './control-administrative-form' +import { MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE } from './use-add-update-control.tsx' +import { DELETE_CONTROL_ADMINISTRATIVE } from './use-delete-control.tsx' + +vi.mock('react-router-dom', async importOriginal => { + const actual = await importOriginal() + return { + ...actual, + useParams: (): Readonly> => ({ missionId: '761', actionId: '3434' }) + } +}) + +const MOCK_DATA_GET_ACTION_BY_ID = { + data: { + id: '', + startDateTimeUtc: '2024-01-01T00:00:00Z', + endDateTimeUtc: '2024-01-12T01:00:00Z', + longitude: 3, + latitude: 5, + observations: '', + detectedPollution: false, + pollutionObservedByAuthorizedAgent: false, + diversionCarriedOut: false, + isSimpleBrewingOperationDone: false, + isAntiPolDeviceDeployed: false + } as ActionAntiPollution +} + +const MOCK_DATA_GET_MISSION_TIMELINE = { + mission: { + id: '762', + startDateTimeUtc: '2024-01-09T09:00Z', + endDateTimeUtc: '2024-01-09T15:00Z', + status: null, + completenessForStats: null, + actions: [ + { + id: '763', + type: 'CONTROL', + source: 'MONITORFISH', + status: 'UNKNOWN', + summaryTags: ['Sans PV', '4 NATINF'], + startDateTimeUtc: '2024-01-09T14:00Z', + endDateTimeUtc: null, + completenessForStats: { + status: 'INCOMPLETE', + sources: ['RAPPORTNAV'], + __typename: 'CompletenessForStats' + }, + data: { + id: '763', + actionDatetimeUtc: '2024-01-09T14:00Z', + actionType: 'LAND_CONTROL', + vesselId: 5232556, + vesselName: 'Le Stella', + controlsToComplete: ['ADMINISTRATIVE', 'SECURITY'], + __typename: 'FishActionData' + }, + __typename: 'Action' + }, + { + id: '226d84bc-e6c5-4d29-8a5f-7db642f99762', + type: 'CONTROL', + source: 'MONITORENV', + status: 'UNKNOWN', + summaryTags: ['Sans PV', '2 NATINF'], + startDateTimeUtc: '2024-01-09T10:00Z', + endDateTimeUtc: null, + completenessForStats: { + status: 'INCOMPLETE', + sources: ['RAPPORTNAV', 'MONITORENV'], + __typename: 'CompletenessForStats' + }, + data: { + id: '226d84bc-e6c5-4d29-8a5f-7db642f99762', + actionNumberOfControls: 2, + actionTargetType: 'VEHICLE', + vehicleType: 'VESSEL', + controlsToComplete: ['NAVIGATION', 'SECURITY'], + formattedControlPlans: null, + __typename: 'EnvActionData' + }, + __typename: 'Action' + }, + { + id: '226d84bc-e6c5-4d29-8a5f-799642f99762', + type: 'SURVEILLANCE', + source: 'MONITORENV', + status: 'UNKNOWN', + summaryTags: ['Sans PV', 'Sans infraction'], + startDateTimeUtc: '2024-01-09T12:00Z', + endDateTimeUtc: '2024-01-09T13:00Z', + completenessForStats: { + status: 'COMPLETE', + sources: [], + __typename: 'CompletenessForStats' + }, + data: { + id: '226d84bc-e6c5-4d29-8a5f-799642f99762', + actionNumberOfControls: null, + actionTargetType: null, + vehicleType: null, + controlsToComplete: [], + formattedControlPlans: null, + __typename: 'EnvActionData' + }, + __typename: 'Action' + }, + { + id: '8cea3f9e-fc6c-433a-8de4-e8d664ea25ed', + type: 'CONTROL', + source: 'RAPPORTNAV', + status: 'UNKNOWN', + summaryTags: ['Sans PV', 'Sans infraction'], + startDateTimeUtc: '2024-08-19T14:05Z', + endDateTimeUtc: '2024-08-19T14:05Z', + completenessForStats: { + status: 'INCOMPLETE', + sources: null, + __typename: 'CompletenessForStats' + }, + data: { + id: '8cea3f9e-fc6c-433a-8de4-e8d664ea25ed', + controlMethod: 'SEA', + vesselIdentifier: null, + vesselType: 'FISHING', + vesselSize: null, + __typename: 'NavActionControl' + }, + __typename: 'Action' + } + ], + __typename: 'Mission' + } +} + +const MOCK_DATA_MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE = { + addOrUpdateControlAdministrative: { + id: 'bf2ace26-1897-4c69-84f4-1b0d1e275eba', + amountOfControls: 1, + unitShouldConfirm: null, + unitHasConfirmed: true, + compliantOperatingPermit: null, + upToDateNavigationPermit: null, + compliantSecurityDocuments: 'NO', + observations: null, + __typename: 'ControlAdministrative' + } +} +const MOCK_DATA_DELETE_CONTROL_ADMINISTRATIVE = { deleteControlAdministrative: true } + +type ControlAdministrativeMockProps = { + unitShouldConfirm?: boolean + data?: ControlAdministrative + shouldCompleteControl?: boolean + mocks?: ReadonlyArray> +} + +const renderControlAdministrativeForm = ({ mocks, ...props }: ControlAdministrativeMockProps) => + render( + + + {} + + + ) + +describe('ControlAdministrativeForm', () => { + it('should render control administrative', () => { + const data = { + id: '', + amountOfControls: 0 + } as ControlAdministrative + renderControlAdministrativeForm({ data }) + expect(screen.getByText('Permis de mise en exploitation (autorisation à pêcher) conforme')).toBeInTheDocument() + }) + + it('it should have control title check when should complete control is true', () => { + const data = { + id: '', + amountOfControls: 0 + } as ControlAdministrative + const wrapper = renderControlAdministrativeForm({ data, shouldCompleteControl: false }) + const checkbox = wrapper.container.querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement + expect(checkbox).toBeChecked() + }) + + it('it should display required red ', () => { + const wrapper = renderControlAdministrativeForm({ shouldCompleteControl: true }) + expect(wrapper.getAllByTestId('control-title-required-control')).not.toBeNull() + }) + + it('it should show unit should confirm toogle', () => { + const data = { + id: '', + amountOfControls: 0 + } as ControlAdministrative + renderControlAdministrativeForm({ data, unitShouldConfirm: true }) + expect(screen.getByText('Contrôle confirmé par l’unité')).toBeInTheDocument() + }) + + it('it should trigger mutate control', async () => { + const addOrUpdateControlMatcher = vi.fn().mockReturnValue(true) + const mocks = [ + { + request: { + query: MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE + }, + variableMatcher: addOrUpdateControlMatcher, + result: { + data: MOCK_DATA_MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE + } + }, + { + request: { + query: GET_ACTION_BY_ID + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_ACTION_BY_ID + } + }, + { + request: { + query: GET_MISSION_TIMELINE + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_MISSION_TIMELINE + } + } + ] + const wrapper = renderControlAdministrativeForm({ mocks, shouldCompleteControl: false, unitShouldConfirm: false }) + const checkbox = wrapper.container.querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement + fireEvent.click(checkbox) + await waitFor(() => { + expect(addOrUpdateControlMatcher).toHaveBeenCalledTimes(1) + }) + }) + + it('it should trigger delete control', async () => { + const deleteControlMock = vi.fn().mockReturnValue(true) + const mocks = [ + { + request: { + query: DELETE_CONTROL_ADMINISTRATIVE + }, + variableMatcher: deleteControlMock, + result: { + data: MOCK_DATA_DELETE_CONTROL_ADMINISTRATIVE + } + }, + { + request: { + query: GET_ACTION_BY_ID + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_ACTION_BY_ID + } + }, + { + request: { + query: GET_MISSION_TIMELINE + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_MISSION_TIMELINE + } + } + ] + const data = { + id: 'scscss', + observations: 'myObservations', + amountOfControls: 0, + compliantSecurityDocuments: ControlResult.NOT_CONTROLLED, + upToDateNavigationPermit: ControlResult.NO, + compliantOperatingPermit: ControlResult.NOT_CONCERNED + } as ControlAdministrative + + const wrapper = renderControlAdministrativeForm({ + mocks, + data, + shouldCompleteControl: true, + unitShouldConfirm: false + }) + const checkbox = wrapper.container.querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement + fireEvent.click(checkbox) + await waitFor(() => { + expect(deleteControlMock).toHaveBeenCalledTimes(1) + }) + }) + + it('it should update form and trigger mutate control 5 secondes after', async () => { + vi.useFakeTimers({ shouldAdvanceTime: true }) + const addOrUpdateControlMatcher = vi.fn().mockReturnValue(true) + const mocks = [ + { + request: { + query: MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE + }, + variableMatcher: addOrUpdateControlMatcher, + result: { + data: MOCK_DATA_MUTATION_ADD_OR_UPDATE_CONTROL_ADMINISTRATIVE + } + }, + { + request: { + query: GET_ACTION_BY_ID + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_ACTION_BY_ID + } + }, + { + request: { + query: GET_MISSION_TIMELINE + }, + variableMatcher: () => true, + result: { + data: MOCK_DATA_GET_MISSION_TIMELINE + } + } + ] + const wrapper = renderControlAdministrativeForm({ mocks, shouldCompleteControl: false, unitShouldConfirm: false }) + const radio = wrapper.container.querySelectorAll("input[type='radio']")[0] as HTMLInputElement + fireEvent.click(radio) + expect(addOrUpdateControlMatcher).toHaveBeenCalledTimes(0) + vi.advanceTimersByTime(5000) + await waitFor(() => { + expect(addOrUpdateControlMatcher).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.tsx b/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.tsx index e9a38443..052cb0dc 100644 --- a/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.tsx +++ b/frontend/src/features/pam/mission/components/elements/controls/control-administrative-form.tsx @@ -1,19 +1,30 @@ -import { Panel, Stack, Toggle } from 'rsuite' -import { ControlAdministrative, ControlType } from '../../../../../common/types/control-types.ts' -import { Label, MultiRadio, OptionValue, Textarea, THEME } from '@mtes-mct/monitor-ui' +import { FormikEffect, FormikMultiRadio, FormikTextarea, FormikToggle, Label, THEME } from '@mtes-mct/monitor-ui' +import { Form, Formik } from 'formik' +import _ from 'lodash' import omit from 'lodash/omit' -import { controlResultOptions } from './control-result.ts' +import { FC, useEffect, useRef, useState } from 'react' import { useParams } from 'react-router-dom' -import ControlTitleCheckbox from '../../ui/control-title-checkbox.tsx' -import ControlInfraction from '../infractions/infraction-for-control.tsx' -import { FC, useEffect, useState } from 'react' -import useDeleteControl from '../../../hooks/use-delete-control.tsx' -import useAddOrUpdateControl from '../../../hooks/use-add-update-control.tsx' +import { Panel, Stack } from 'rsuite' +import { ControlAdministrative, ControlResult, ControlType } from '../../../types/control-types' +import ControlInfraction from '../infractions/infraction-for-control' +import { controlResultOptions } from './control-result' +import ControlTitleCheckbox from './control-title-checkbox' +import useAddOrUpdateControl from './use-add-update-control.tsx' +import useDeleteControl from './use-delete-control.tsx' +const DEBOUNCE_TIME_TRIGGER = 5000 + +export type ControlAdministrativeFormInput = { + observations?: string + unitHasConfirmed?: boolean + compliantOperatingPermit?: ControlResult + upToDateNavigationPermit?: ControlResult + compliantSecurityDocuments?: ControlResult +} interface ControlAdministrativeFormProps { + unitShouldConfirm?: boolean data?: ControlAdministrative shouldCompleteControl?: boolean - unitShouldConfirm?: boolean } const ControlAdministrativeForm: FC = ({ @@ -22,67 +33,71 @@ const ControlAdministrativeForm: FC = ({ unitShouldConfirm }) => { const { missionId, actionId } = useParams() - - const [observationsValue, setObservationsValue] = useState(data?.observations) - const controlOptions = controlResultOptions() + const [isRequired, setIsRequired] = useState() + const timerRef = useRef>() + const [checkedControl, setCheckedControl] = useState() + const [control, setControl] = useState() - const handleObservationsChange = (nextValue?: string) => { - setObservationsValue(nextValue) - } + const [deleteControl] = useDeleteControl({ controlType: ControlType.ADMINISTRATIVE }) + const [mutateControl] = useAddOrUpdateControl({ controlType: ControlType.ADMINISTRATIVE }) + + const getControlInput = (data?: ControlAdministrative) => + data + ? _.omitBy( + _.pick( + data, + 'observations', + 'unitHasConfirmed', + 'compliantOperatingPermit', + 'upToDateNavigationPermit', + 'compliantSecurityDocuments' + ), + _.isNull + ) + : ({} as ControlAdministrativeFormInput) useEffect(() => { - setObservationsValue(data?.observations) - }, [data]) + setControl(getControlInput(data)) + setIsRequired(!!shouldCompleteControl && !!!data) + setCheckedControl(!!data || shouldCompleteControl) + }, [data, shouldCompleteControl]) - const handleObservationsBlur = async () => { - if (observationsValue !== data?.observations) { - await onChange('observations', observationsValue) - } + const handleChange = async (value: ControlAdministrativeFormInput): Promise => { + clearTimeout(timerRef.current) + if (value === control) return + timerRef.current = setTimeout(() => handleUpdate(value), DEBOUNCE_TIME_TRIGGER) } - const [mutateControl, { loading: mutationLoading }] = useAddOrUpdateControl({ - controlType: ControlType.ADMINISTRATIVE - }) - const [deleteControl] = useDeleteControl({ controlType: ControlType.ADMINISTRATIVE }) - - const toggleControl = async (isChecked: boolean) => - isChecked - ? onChange() - : await deleteControl({ - variables: { - actionId - } - }) - - const onChange = async (field?: string, value?: any) => { - let updatedData = { - ...omit(data, '__typename', 'infractions'), - id: data?.id, - missionId: missionId, - actionControlId: actionId, - amountOfControls: 1, - unitShouldConfirm: unitShouldConfirm - } - - if (!!field && value !== undefined) { - updatedData = { - ...updatedData, - [field]: value + const handleUpdate = async (value?: ControlAdministrativeFormInput) => { + if (!value) return + if (!_.isBoolean(value.unitHasConfirmed)) value.unitHasConfirmed = true + let variables = { + control: { + ...omit(data, 'infractions'), + missionId, + unitShouldConfirm, + amountOfControls: 1, + actionControlId: actionId, + ...value } } + await mutateControl({ variables }) + } - await mutateControl({ variables: { control: updatedData } }) + const toggleControl = async (isChecked: boolean) => { + setCheckedControl(isChecked) + const variables = { actionId } + isChecked ? await handleUpdate(control) : await deleteControl({ variables }) } return ( toggleControl(isChecked)} /> } @@ -91,72 +106,62 @@ const ControlAdministrativeForm: FC = ({ style={{ backgroundColor: THEME.color.white, borderRadius: 0 }} > - {unitShouldConfirm && ( - - - - {/* TODO add Toggle component to monitor-ui */} - onChange('unitHasConfirmed', checked)} - disabled={mutationLoading} - /> - - - - - - + {control !== undefined && ( + + <> + +
+ {unitShouldConfirm && ( + + + + + + + + + + + )} + + + + + + + + + + + + + +
+ +
)} - - onChange('compliantOperatingPermit', nextValue)} - options={controlOptions} - disabled={mutationLoading} - /> - - - onChange('upToDateNavigationPermit', nextValue)} - options={controlOptions} - disabled={mutationLoading} - /> - - - onChange('compliantSecurityDocuments', nextValue)} - options={controlOptions} - disabled={mutationLoading} - /> - - -