From 3e3f7d3efad05ea199b0bc98feba5354e82c8de7 Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 22 Nov 2023 13:13:19 +0100 Subject: [PATCH] Add control unit dialogs on map [WIP] --- frontend/.eslintrc.js | 2 +- frontend/package-lock.json | 58 +++-- frontend/package.json | 4 +- frontend/src/api/administration.ts | 33 +++ frontend/src/api/constants.ts | 9 + frontend/src/api/controlUnit.ts | 31 ++- frontend/src/api/controlUnitContact.ts | 57 +++++ frontend/src/api/controlUnitResource.ts | 95 ++++++++ frontend/src/api/index.ts | 22 +- frontend/src/api/legacyControlUnit.ts | 14 ++ frontend/src/api/mission.ts | 12 +- frontend/src/api/station.ts | 27 +++ frontend/src/api/types.ts | 20 ++ frontend/src/components/ConfirmationModal.tsx | 87 +++++++ frontend/src/components/Dialog.tsx | 82 +++++++ frontend/src/components/OverlayCard.tsx | 39 +++ frontend/src/constants/index.ts | 2 + .../src/domain/entities/controlUnits/utils.ts | 6 +- frontend/src/domain/entities/mission/types.ts | 6 +- .../shared_slices/DisplayedComponent.ts | 29 +-- frontend/src/domain/types/controlResource.ts | 4 - frontend/src/domain/types/controlUnit.ts | 25 -- .../src/domain/types/legacyControlUnit.ts | 23 ++ frontend/src/domain/types/missionAction.ts | 4 +- frontend/src/domain/types/reporting.ts | 6 +- .../components/ControlUnitDialog/AreaNote.tsx | 35 +++ .../ControlUnitContactList/Form.tsx | 98 ++++++++ .../FormikNameSelect.tsx | 81 +++++++ .../ControlUnitContactList/Item.tsx | 63 +++++ .../ControlUnitContactList/constants.ts | 42 ++++ .../ControlUnitContactList/index.tsx | 167 +++++++++++++ .../ControlUnitContactList/types.ts | 7 + .../ControlUnitContactList/utils.ts | 26 ++ .../ControlUnitResourceList/Form.tsx | 134 +++++++++++ .../ControlUnitResourceList/Item.tsx | 65 +++++ .../ControlUnitResourceList/Placeholder.tsx | 25 ++ .../ControlUnitResourceList/constants.ts | 24 ++ .../ControlUnitResourceList/index.tsx | 227 ++++++++++++++++++ .../ControlUnitResourceList/types.ts | 5 + .../ControlUnitResourceList/utils.tsx | 39 +++ .../components/ControlUnitDialog/index.tsx | 92 +++++++ .../ControlUnitDialog/shared/Section.tsx | 23 ++ .../ControlUnitDialog/shared/TextareaForm.tsx | 90 +++++++ .../components/ControlUnitDialog/slice.ts | 25 ++ .../ControlUnitListDialog/FilterBar.tsx | 128 ++++++++++ .../components/ControlUnitListDialog/Item.tsx | 69 ++++++ .../ControlUnitListDialog/index.tsx | 47 ++++ .../components/ControlUnitListDialog/slice.ts | 39 +++ .../components/ControlUnitListDialog/types.ts | 6 + .../components/ControlUnitListDialog/utils.ts | 118 +++++++++ .../ControlUnitSelect.tsx | 43 ++-- .../FormikMultiControlUnitPicker/index.tsx | 11 +- .../FormikMultiControlUnitPicker/utils.ts | 17 +- .../SideWindow/MissionForm/constants.tsx | 4 +- .../features/SideWindow/MissionForm/types.ts | 4 +- .../features/SideWindow/MissionForm/utils.tsx | 12 +- .../ExportActivityReportsDialog/types.ts | 4 +- .../SideWindow/MissionList/FilterBar.tsx | 4 +- .../features/SideWindow/MissionList/utils.tsx | 4 +- .../Reportings/Current/ReportingForm.tsx | 4 +- .../VesselSidebar/Reportings/Current/utils.ts | 6 +- .../Reportings/ReportingCard.tsx | 4 +- .../Mission/MissionsLabelsLayer/utils.ts | 4 +- .../MissionOverlay/MissionDetails.tsx | 4 +- frontend/src/libs/FrontendApiError.ts | 28 +++ frontend/src/libs/FrontendError.ts | 36 ++- frontend/src/libs/UserError.ts | 25 ++ frontend/src/react-app-env.d.ts | 1 - frontend/src/store/index.ts | 8 +- frontend/src/store/reducers.ts | 4 + frontend/src/store/redux-persist.d.ts | 15 -- frontend/src/utils/isEmptyish.ts | 9 + frontend/src/utils/isNotArchived.ts | 3 + .../utils/sortCollectionByLocalizedProps.ts | 31 +++ 74 files changed, 2476 insertions(+), 181 deletions(-) create mode 100644 frontend/src/api/administration.ts create mode 100644 frontend/src/api/controlUnitContact.ts create mode 100644 frontend/src/api/controlUnitResource.ts create mode 100644 frontend/src/api/legacyControlUnit.ts create mode 100644 frontend/src/api/station.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/components/ConfirmationModal.tsx create mode 100644 frontend/src/components/Dialog.tsx create mode 100644 frontend/src/components/OverlayCard.tsx delete mode 100644 frontend/src/domain/types/controlResource.ts delete mode 100644 frontend/src/domain/types/controlUnit.ts create mode 100644 frontend/src/domain/types/legacyControlUnit.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/AreaNote.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/Form.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/FormikNameSelect.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/Item.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/constants.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/index.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/types.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/utils.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Form.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Item.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/Placeholder.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/constants.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/index.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/types.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitResourceList/utils.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/index.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/shared/Section.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/shared/TextareaForm.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitDialog/slice.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/FilterBar.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/Item.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/index.tsx create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/slice.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/types.ts create mode 100644 frontend/src/features/ControlUnit/components/ControlUnitListDialog/utils.ts create mode 100644 frontend/src/libs/FrontendApiError.ts create mode 100644 frontend/src/libs/UserError.ts delete mode 100644 frontend/src/react-app-env.d.ts delete mode 100644 frontend/src/store/redux-persist.d.ts create mode 100644 frontend/src/utils/isEmptyish.ts create mode 100644 frontend/src/utils/isNotArchived.ts create mode 100644 frontend/src/utils/sortCollectionByLocalizedProps.ts diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index a142fa32e9..8955f64fbb 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -126,7 +126,7 @@ module.exports = { // UI { - files: ['src/ui/**/*.tsx'], + files: ['src/components/**/*.tsx', 'src/ui/**/*.tsx'], rules: { 'react/jsx-props-no-spreading': 'off' } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bd49ec2a78..79e0c1b48c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@dnd-kit/core": "6.0.8", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "10.9.3", + "@mtes-mct/monitor-ui": "10.13.0", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", @@ -53,7 +53,7 @@ "redux": "4.2.1", "redux-persist": "6.0.0", "redux-thunk": "2.4.2", - "rsuite": "5.37.0", + "rsuite": "5.45.0", "rsuite-table": "5.12.0", "simplify-geojson": "1.0.5", "styled-components": "5.3.11", @@ -3800,9 +3800,9 @@ "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, "node_modules/@mtes-mct/monitor-ui": { - "version": "10.9.3", - "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-10.9.3.tgz", - "integrity": "sha512-I+P+QQYW/7I9oyhpzkePOiV4ucfoKHazKz8pohWeLy4Ode4xWjGgXLC7pi2EMswrWlXu3O7+t9ViUdSicK2ryg==", + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/@mtes-mct/monitor-ui/-/monitor-ui-10.13.0.tgz", + "integrity": "sha512-3F0Eiyij01VRVz4zt9dKm1ItNzpnUXytVTxXJvPixh/hhUM/B8jjDn+8qVT6gDyNM3Ojm+jLPzGIeHrCBRJqgQ==", "dependencies": { "@babel/runtime": "7.22.15", "@tanstack/react-table": "8.9.7", @@ -3811,7 +3811,7 @@ "tslib": "2.6.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "peerDependencies": { "@sentry/react": "^7.0.0", @@ -3819,7 +3819,7 @@ "formik": "^2.0.0", "react": "^18.0.0", "react-router-dom": "^6.0.0", - "rsuite": "^5.37.0", + "rsuite": "^5.45.0", "styled-components": "^5.0.0 || ^6.0.0" } }, @@ -4887,9 +4887,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==" + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==" }, "node_modules/@types/debug": { "version": "4.1.12", @@ -5039,9 +5039,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.201", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", - "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==" + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, "node_modules/@types/mdast": { "version": "3.0.15", @@ -15796,9 +15796,9 @@ } }, "node_modules/rsuite": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.37.0.tgz", - "integrity": "sha512-UhsXIQtz0EIPHItqUSij3x6quHd6w4e83awiJyhaIMIcDl3GExCP1YNObmwcrbVbtDlnmiR7I8kjeWAJEAtRPg==", + "version": "5.45.0", + "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.45.0.tgz", + "integrity": "sha512-5k1CBhO/vaotCVaEodIW/Oec+ppFEVxUpOQp5mnReFJ8EPSPnRcbyHUAh9nh5Yj0bw4UFYJOLE27y7uqJctZOQ==", "dependencies": { "@babel/runtime": "^7.20.1", "@juggle/resize-observer": "^3.4.0", @@ -15814,7 +15814,7 @@ "prop-types": "^15.8.1", "react-use-set": "^1.0.0", "react-window": "^1.8.8", - "rsuite-table": "^5.11.0", + "rsuite-table": "^5.17.0", "schema-typed": "^2.1.3" }, "peerDependencies": { @@ -15846,6 +15846,30 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/rsuite/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/rsuite/node_modules/rsuite-table": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/rsuite-table/-/rsuite-table-5.17.0.tgz", + "integrity": "sha512-QfWs2YDxNKL0EmN3+Lf+IsKoNcrYuri57Tpbii98MzKzQbbTCGcZmpE+WlpsnhZv15oFCk37Pi8K+v4sm7ImXg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@juggle/resize-observer": "^3.3.1", + "@rsuite/icons": "^1.0.0", + "classnames": "^2.3.1", + "dom-lib": "^3.1.3", + "lodash": "^4.17.21", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7ef5811a98..716cf6f3ef 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,7 +30,7 @@ "dependencies": { "@dnd-kit/core": "6.0.8", "@dnd-kit/modifiers": "6.0.1", - "@mtes-mct/monitor-ui": "10.9.3", + "@mtes-mct/monitor-ui": "10.13.0", "@reduxjs/toolkit": "1.9.6", "@sentry/browser": "7.55.2", "@sentry/react": "7.52.1", @@ -72,7 +72,7 @@ "redux": "4.2.1", "redux-persist": "6.0.0", "redux-thunk": "2.4.2", - "rsuite": "5.37.0", + "rsuite": "5.45.0", "rsuite-table": "5.12.0", "simplify-geojson": "1.0.5", "styled-components": "5.3.11", diff --git a/frontend/src/api/administration.ts b/frontend/src/api/administration.ts new file mode 100644 index 0000000000..ac1d5f5b32 --- /dev/null +++ b/frontend/src/api/administration.ts @@ -0,0 +1,33 @@ +import { monitorenvApi } from '.' +import { FrontendApiError } from '../libs/FrontendApiError' + +import type { Administration } from '@mtes-mct/monitor-ui' + +export const ARCHIVE_ADMINISTRATION_ERROR_MESSAGE = [ + 'Certaines unités de cette administration ne sont pas archivées.', + 'Veuillez les archiver pour pouvoir archiver cette administration.' +].join(' ') +export const DELETE_ADMINISTRATION_ERROR_MESSAGE = [ + 'Des unités sont encore rattachées à cette administration.', + 'Veuillez les supprimer avant de pouvoir supprimer cette administration.' +].join(' ') +const GET_ADMINISTRATION_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette administration." +const GET_ADMINISTRATIONS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des administrations." + +export const monitorenvAdministrationApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + getAdministration: builder.query({ + providesTags: () => [{ type: 'Administrations' }], + query: administrationId => `/v1/administrations/${administrationId}`, + transformErrorResponse: response => new FrontendApiError(GET_ADMINISTRATION_ERROR_MESSAGE, response) + }), + + getAdministrations: builder.query({ + providesTags: () => [{ type: 'Administrations' }], + query: () => `/v1/administrations`, + transformErrorResponse: response => new FrontendApiError(GET_ADMINISTRATIONS_ERROR_MESSAGE, response) + }) + }) +}) + +export const { useGetAdministrationQuery, useGetAdministrationsQuery } = monitorenvAdministrationApi diff --git a/frontend/src/api/constants.ts b/frontend/src/api/constants.ts index f407aa44b6..035cecf16a 100644 --- a/frontend/src/api/constants.ts +++ b/frontend/src/api/constants.ts @@ -1,3 +1,12 @@ +import { FIVE_MINUTES } from '../constants' + +export const ARCHIVE_GENERIC_ERROR_MESSAGE = 'An unexpected error occurred while attempting to delete this entity.' +export const DELETE_GENERIC_ERROR_MESSAGE = 'An unexpected error occurred while attempting to delete this entity.' + +export const RTK_DEFAULT_QUERY_OPTIONS = { + pollingInterval: FIVE_MINUTES +} + export enum HttpStatusCode { OK = 200, CREATED = 201, diff --git a/frontend/src/api/controlUnit.ts b/frontend/src/api/controlUnit.ts index 0f21f613b1..5f16334f13 100644 --- a/frontend/src/api/controlUnit.ts +++ b/frontend/src/api/controlUnit.ts @@ -1,14 +1,37 @@ import { monitorenvApi } from '.' +import { FrontendApiError } from '../libs/FrontendApiError' -import type { ControlUnit } from '../domain/types/controlUnit' +import type { ControlUnit } from '@mtes-mct/monitor-ui' -export const controlUnitApi = monitorenvApi.injectEndpoints({ +const GET_CONTROL_UNIT_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette unité de contrôle." +const GET_CONTROL_UNITS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des unités de contrôle." +const UPDATE_CONTROL_UNIT_ERROR_MESSAGE = "Nous n'avons pas pu mettre à jour cette unité de contrôle." + +export const monitorenvControlUnitApi = monitorenvApi.injectEndpoints({ endpoints: builder => ({ + getControlUnit: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: controlUnitId => `/v2/control_units/${controlUnitId}`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNIT_ERROR_MESSAGE, response) + }), + getControlUnits: builder.query({ providesTags: () => [{ type: 'ControlUnits' }], - query: () => `/control_units` + query: () => `/v2/control_units`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNITS_ERROR_MESSAGE, response) + }), + + updateControlUnit: builder.mutation({ + invalidatesTags: () => [{ type: 'Administrations' }, { type: 'ControlUnits' }], + query: nextControlUnitData => ({ + body: nextControlUnitData, + method: 'PUT', + url: `/v2/control_units/${nextControlUnitData.id}` + }), + transformErrorResponse: response => new FrontendApiError(UPDATE_CONTROL_UNIT_ERROR_MESSAGE, response) }) }) }) -export const { useGetControlUnitsQuery } = controlUnitApi +export const { useGetControlUnitQuery, useGetControlUnitsQuery, useUpdateControlUnitMutation } = + monitorenvControlUnitApi diff --git a/frontend/src/api/controlUnitContact.ts b/frontend/src/api/controlUnitContact.ts new file mode 100644 index 0000000000..eb2e5a33cf --- /dev/null +++ b/frontend/src/api/controlUnitContact.ts @@ -0,0 +1,57 @@ +import { monitorenvApi } from '.' +import { FrontendApiError } from '../libs/FrontendApiError' + +import type { ControlUnit } from '@mtes-mct/monitor-ui' + +const GET_CONTROL_UNIT_CONTACT_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette contact." +const GET_CONTROL_UNIT_CONTACTS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des contacts." + +export const monitorenvControlUnitContactApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + createControlUnitContact: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }], + query: newControlUnitContactData => ({ + body: newControlUnitContactData, + method: 'POST', + url: `/v1/control_unit_contacts` + }) + }), + + deleteControlUnitContact: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }], + query: controlUnitContactId => ({ + method: 'DELETE', + url: `/v1/control_unit_contacts/${controlUnitContactId}` + }) + }), + + getControlUnitContact: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: controlUnitContactId => `/v1/control_unit_contacts/${controlUnitContactId}`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNIT_CONTACT_ERROR_MESSAGE, response) + }), + + getControlUnitContacts: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: () => `/v1/control_unit_contacts`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNIT_CONTACTS_ERROR_MESSAGE, response) + }), + + updateControlUnitContact: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }], + query: nextControlUnitContactData => ({ + body: nextControlUnitContactData, + method: 'PUT', + url: `/v1/control_unit_contacts/${nextControlUnitContactData.id}` + }) + }) + }) +}) + +export const { + useCreateControlUnitContactMutation, + useDeleteControlUnitContactMutation, + useGetControlUnitContactQuery, + useGetControlUnitContactsQuery, + useUpdateControlUnitContactMutation +} = monitorenvControlUnitContactApi diff --git a/frontend/src/api/controlUnitResource.ts b/frontend/src/api/controlUnitResource.ts new file mode 100644 index 0000000000..769b2016c2 --- /dev/null +++ b/frontend/src/api/controlUnitResource.ts @@ -0,0 +1,95 @@ +import { monitorenvApi } from '.' +import { ARCHIVE_GENERIC_ERROR_MESSAGE } from './constants' +import { ApiErrorCode, type BackendApiBooleanResponse } from './types' +import { FrontendApiError } from '../libs/FrontendApiError' +import { newUserError } from '../libs/UserError' + +import type { ControlUnit } from '@mtes-mct/monitor-ui' + +export const ARCHIVE_CONTROL_UNITE_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu archiver ce moyen." +const CAN_DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu vérifier si ce moyen est supprimable." +export const DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = + "Ce moyen est rattaché à des missions. Veuillez l'en détacher avant de le supprimer." +const GET_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE = "Nous n'avons pas pu récupérer ce moyen." +const GET_CONTROL_UNIT_RESOURCES_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des moyens." + +export const monitorenvControlUnitResourceApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + archiveControlUnitResource: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }, { type: 'Stations' }], + query: controlUnitResourceId => ({ + method: 'PUT', + url: `/v1/control_unit_resources/${controlUnitResourceId}/archive` + }), + transformErrorResponse: response => { + if (response.data.type === ApiErrorCode.UNARCHIVED_CHILD) { + return newUserError(ARCHIVE_CONTROL_UNITE_RESOURCE_ERROR_MESSAGE) + } + + return new FrontendApiError(ARCHIVE_GENERIC_ERROR_MESSAGE, response) + } + }), + + canDeleteControlUnitResource: builder.query({ + query: controlUnitResourceId => `/v1/control_unit_resources/${controlUnitResourceId}/can_delete`, + transformErrorResponse: response => + new FrontendApiError(CAN_DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response), + transformResponse: (response: BackendApiBooleanResponse) => response.value + }), + + createControlUnitResource: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }, { type: 'Stations' }], + query: newControlUnitResourceData => ({ + body: newControlUnitResourceData, + method: 'POST', + url: `/v1/control_unit_resources` + }) + }), + + deleteControlUnitResource: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }, { type: 'Stations' }], + query: controlUnitResourceId => ({ + method: 'DELETE', + url: `/v1/control_unit_resources/${controlUnitResourceId}` + }), + transformErrorResponse: response => { + if (response.data.type === ApiErrorCode.FOREIGN_KEY_CONSTRAINT) { + return newUserError(DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE) + } + + return new FrontendApiError(DELETE_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response) + } + }), + + getControlUnitResource: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: controlUnitResourceId => `/v1/control_unit_resources/${controlUnitResourceId}`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNIT_RESOURCE_ERROR_MESSAGE, response) + }), + + getControlUnitResources: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: () => `/v1/control_unit_resources`, + transformErrorResponse: response => new FrontendApiError(GET_CONTROL_UNIT_RESOURCES_ERROR_MESSAGE, response) + }), + + updateControlUnitResource: builder.mutation({ + invalidatesTags: () => [{ type: 'ControlUnits' }, { type: 'Stations' }], + query: nextControlUnitResourceData => ({ + body: nextControlUnitResourceData, + method: 'PUT', + url: `/v1/control_unit_resources/${nextControlUnitResourceData.id}` + }) + }) + }) +}) + +export const { + useArchiveControlUnitResourceMutation, + useCanDeleteControlUnitResourceQuery, + useCreateControlUnitResourceMutation, + useDeleteControlUnitResourceMutation, + useGetControlUnitResourceQuery, + useGetControlUnitResourcesQuery, + useUpdateControlUnitResourceMutation +} = monitorenvControlUnitResourceApi diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 92c43691e0..86b9f2fddf 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -8,6 +8,8 @@ import ky from 'ky' import { getOIDCUser } from '../auth/getOIDCUser' import { normalizeRtkBaseQuery } from '../utils/normalizeRtkBaseQuery' +import type { BackendApiErrorResponse } from './types' + const MAX_RETRIES = 2 // Using local MonitorEnv stubs: @@ -20,18 +22,30 @@ const MONITORENV_API_URL = import.meta.env.VITE_MONITORENV_URL // Monitorenv API // We'll need that later on if we use any kind of authentication. -const monitorenvBaseQuery = retry( +const monitorenvApiBaseQuery = retry( fetchBaseQuery({ - baseUrl: `${MONITORENV_API_URL}/api/v1` + baseUrl: `${MONITORENV_API_URL}/api` }), { maxRetries: MAX_RETRIES } ) export const monitorenvApi = createApi({ - baseQuery: normalizeRtkBaseQuery(monitorenvBaseQuery), + baseQuery: async (args, api, extraOptions) => { + const result = await normalizeRtkBaseQuery(monitorenvApiBaseQuery)(args, api, extraOptions) + if (result.error) { + return { + error: { + data: result.error.data as BackendApiErrorResponse, + status: result.error.status + } + } + } + + return result + }, endpoints: () => ({}), reducerPath: 'monitorenvApi', - tagTypes: ['ControlUnits', 'Missions'] + tagTypes: ['Administrations', 'ControlUnits', 'Missions', 'Stations'] }) // ============================================================================= diff --git a/frontend/src/api/legacyControlUnit.ts b/frontend/src/api/legacyControlUnit.ts new file mode 100644 index 0000000000..66162f0bf0 --- /dev/null +++ b/frontend/src/api/legacyControlUnit.ts @@ -0,0 +1,14 @@ +import { monitorenvApi } from '.' + +import type { LegacyControlUnit } from '../domain/types/legacyControlUnit' + +export const legacyControlUnitApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + getLegacyControlUnits: builder.query({ + providesTags: () => [{ type: 'ControlUnits' }], + query: () => `/v1/control_units` + }) + }) +}) + +export const { useGetLegacyControlUnitsQuery } = legacyControlUnitApi diff --git a/frontend/src/api/mission.ts b/frontend/src/api/mission.ts index 786e6694fa..b85f4c7729 100644 --- a/frontend/src/api/mission.ts +++ b/frontend/src/api/mission.ts @@ -2,7 +2,7 @@ import { monitorenvApi, monitorfishApi } from '.' import { ApiError } from '../libs/ApiError' import type { Mission, MissionWithActions } from '../domain/entities/mission/types' -import type { ControlUnit } from '../domain/types/controlUnit' +import type { ControlUnit } from '@mtes-mct/monitor-ui' const GET_MISSION_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la mission" const GET_ENGAGED_CONTROL_UNITS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer les unités en mission" @@ -18,7 +18,7 @@ export const monitorenvMissionApi = monitorenvApi.injectEndpoints({ query: mission => ({ body: mission, method: 'POST', - url: `/missions` + url: `/v1/missions` }) }), @@ -30,18 +30,18 @@ export const monitorenvMissionApi = monitorenvApi.injectEndpoints({ }, query: id => ({ method: 'DELETE', - url: `/missions/${id}` + url: `/v1/missions/${id}` }) }), getEngagedControlUnits: builder.query({ - query: () => `missions/engaged_control_units`, + query: () => `/v1/missions/engaged_control_units`, transformErrorResponse: response => new ApiError(GET_ENGAGED_CONTROL_UNITS_ERROR_MESSAGE, response) }), getMission: builder.query({ providesTags: [{ type: 'Missions' }], - query: id => `missions/${id}`, + query: id => `/v1/missions/${id}`, transformErrorResponse: response => new ApiError(GET_MISSION_ERROR_MESSAGE, response) }), @@ -50,7 +50,7 @@ export const monitorenvMissionApi = monitorenvApi.injectEndpoints({ query: mission => ({ body: mission, method: 'POST', - url: `/missions/${mission.id}` + url: `/v1/missions/${mission.id}` }) }) }) diff --git a/frontend/src/api/station.ts b/frontend/src/api/station.ts new file mode 100644 index 0000000000..1d71d82514 --- /dev/null +++ b/frontend/src/api/station.ts @@ -0,0 +1,27 @@ +import { monitorenvApi } from '.' +import { FrontendApiError } from '../libs/FrontendApiError' + +import type { Station } from '@mtes-mct/monitor-ui' + +export const DELETE_STATION_ERROR_MESSAGE = + "Cette base est rattachée à des moyens. Veuillez l'en détacher avant de la supprimer ou bien l'archiver." +const GET_STATION_ERROR_MESSAGE = "Nous n'avons pas pu récupérer cette base." +const GET_STATIONS_ERROR_MESSAGE = "Nous n'avons pas pu récupérer la liste des bases." + +export const monitorenvStationApi = monitorenvApi.injectEndpoints({ + endpoints: builder => ({ + getStation: builder.query({ + providesTags: () => [{ type: 'Stations' }], + query: stationId => `/v1/stations/${stationId}`, + transformErrorResponse: response => new FrontendApiError(GET_STATION_ERROR_MESSAGE, response) + }), + + getStations: builder.query({ + providesTags: () => [{ type: 'Stations' }], + query: () => `/v1/stations`, + transformErrorResponse: response => new FrontendApiError(GET_STATIONS_ERROR_MESSAGE, response) + }) + }) +}) + +export const { useGetStationQuery, useGetStationsQuery } = monitorenvStationApi diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000000..d6e76a15af --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,20 @@ +// Don't forget to mirror any update here in the backend enum. +export enum ApiErrorCode { + /** Thrown when attempting to delete an entity which has to non-archived children. */ + FOREIGN_KEY_CONSTRAINT = 'FOREIGN_KEY_CONSTRAINT', + /** Thrown when attempting to archive an entity linked to non-archived children. */ + UNARCHIVED_CHILD = 'UNARCHIVED_CHILD' +} + +export interface BackendApiErrorResponse { + type: ApiErrorCode | null +} + +export interface BackendApiBooleanResponse { + value: boolean +} + +export interface CustomRTKErrorResponse { + data: BackendApiErrorResponse + status: number | 'FETCH_ERROR' | 'PARSING_ERROR' | 'TIMEOUT_ERROR' | 'CUSTOM_ERROR' +} diff --git a/frontend/src/components/ConfirmationModal.tsx b/frontend/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000000..20c8b9e31d --- /dev/null +++ b/frontend/src/components/ConfirmationModal.tsx @@ -0,0 +1,87 @@ +import { Accent, Button, Dialog, Icon } from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +import type { Promisable } from 'type-fest' + +export type ConfirmationModalProps = { + color?: string + confirmationButtonLabel: string + iconName?: keyof typeof Icon + message: string + onCancel: () => Promisable + onConfirm: () => Promisable + title: string +} +export function ConfirmationModal({ + color, + confirmationButtonLabel, + iconName, + message, + onCancel, + onConfirm, + title +}: ConfirmationModalProps) { + const SelectedIcon = iconName ? Icon[iconName] : null + + return ( + + {title} + + {SelectedIcon && ( + + + + )} + {message} + + + + + + + ) +} + +// TODO Allow direct `width` prop control in MUI. +// This is a mess. I wonder if we should add inner classes in MUI. +const StyledDialog = styled(Dialog)` + > div:last-child { + max-width: 440px; + min-width: 440px; + + /* Dialog.Body */ + > div:nth-child(2) { + padding: 24px 40px 8px; + } + + /* Dialog.Action */ + > div:last-child { + padding: 24px 0 32px; + + > .Element-Button { + width: 136px; + + :not(:first-child) { + margin-left: 8px; + } + } + } + } +` + +const Picto = styled.div` + display: flex; + justify-content: center; + margin-bottom: 8px; +` + +/* TODO Replace the `> p` forcing the `!important`. */ +const Message = styled.p<{ + $color: string | undefined +}>` + ${p => p.$color && `color: ${p.$color} !important;`} + font-size: 16px; + font-weight: bold; +` diff --git a/frontend/src/components/Dialog.tsx b/frontend/src/components/Dialog.tsx new file mode 100644 index 0000000000..6f192601b0 --- /dev/null +++ b/frontend/src/components/Dialog.tsx @@ -0,0 +1,82 @@ +import { Button, Dialog as MuiDialog, Icon } from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +import type { Promisable } from 'type-fest' + +export type DialogProps = { + color?: string + iconName?: keyof typeof Icon + message: string + onClose: () => Promisable + title: string + titleBackgroundColor?: string +} +export function Dialog({ color, iconName, message, onClose, title, titleBackgroundColor }: DialogProps) { + const SelectedIcon = iconName ? Icon[iconName] : null + + return ( + + {title} + + {SelectedIcon && ( + + + + )} + {message} + + + + + + ) +} + +// TODO Allow direct `width` prop control in MUI. +// This is a mess. I wonder if we should add inner classes in MUI. +const StyledDialog = styled(MuiDialog)<{ + $titleBackgroundColor: string | undefined +}>` + > div:last-child { + max-width: 440px; + min-width: 440px; + + /* Dialog.Title */ + > h4 { + ${p => p.$titleBackgroundColor && `background-color: ${p.$titleBackgroundColor};`} + } + + /* Dialog.Body */ + > div:nth-child(2) { + padding: 24px 40px 8px; + } + + /* Dialog.Action */ + > div:last-child { + padding: 24px 0 32px; + + > .Element-Button { + width: 136px; + + :not(:first-child) { + margin-left: 8px; + } + } + } + } +` + +const Picto = styled.div` + display: flex; + justify-content: center; + margin-bottom: 8px; +` + +/* TODO Replace the `> p` forcing the `!important`. */ +const Message = styled.p<{ + $color: string | undefined +}>` + ${p => p.$color && `color: ${p.$color} !important;`} + font-size: 16px; + font-weight: bold; +` diff --git a/frontend/src/components/OverlayCard.tsx b/frontend/src/components/OverlayCard.tsx new file mode 100644 index 0000000000..3ee0ab238d --- /dev/null +++ b/frontend/src/components/OverlayCard.tsx @@ -0,0 +1,39 @@ +import { Icon, MapMenuDialog } from '@mtes-mct/monitor-ui' +import styled from 'styled-components' + +import type { HtmlHTMLAttributes } from 'react' +import type { Promisable } from 'type-fest' + +export type DialogProps = HtmlHTMLAttributes & { + isCloseButtonHidden?: boolean + onClose: () => Promisable + title: string +} +export function OverlayCard({ children, isCloseButtonHidden = false, onClose, title, ...props }: DialogProps) { + return ( + + + {title} + + + {children} + + ) +} + +const StyledMapMenuDialogContainer = styled(MapMenuDialog.Container)` + margin: 0; +` + +const StyledMapMenuDialogTitle = styled(MapMenuDialog.Title)` + display: block; + flex-grow: 1; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; +` diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index b626b65a2d..50115213ca 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -6,3 +6,5 @@ export const BOOLEAN_AS_OPTIONS: Array> = [ { label: 'Oui', value: true }, { label: 'Non', value: false } ] + +export const FIVE_MINUTES = 5 * 60 * 1000 diff --git a/frontend/src/domain/entities/controlUnits/utils.ts b/frontend/src/domain/entities/controlUnits/utils.ts index 8ec38931e0..1b942c559f 100644 --- a/frontend/src/domain/entities/controlUnits/utils.ts +++ b/frontend/src/domain/entities/controlUnits/utils.ts @@ -2,14 +2,14 @@ import { uniq } from 'lodash' import { getOptionsFromStrings } from '../../../utils/getOptionsFromStrings' -import type { ControlUnit } from '../../types/controlUnit' +import type { LegacyControlUnit } from '../../types/legacyControlUnit' import type { Option } from '@mtes-mct/monitor-ui' export function getControlUnitsOptionsFromControlUnits( - controlUnits: ControlUnit.ControlUnit[] | undefined = [], + controlUnits: LegacyControlUnit.LegacyControlUnit[] | undefined = [], selectedAdministrations?: string[] ): { - activeControlUnits: ControlUnit.ControlUnit[] + activeControlUnits: LegacyControlUnit.LegacyControlUnit[] administrationsAsOptions: Option[] unitsAsOptions: Option[] } { diff --git a/frontend/src/domain/entities/mission/types.ts b/frontend/src/domain/entities/mission/types.ts index 87a37e8c4a..3c90b089fc 100644 --- a/frontend/src/domain/entities/mission/types.ts +++ b/frontend/src/domain/entities/mission/types.ts @@ -1,14 +1,14 @@ import { SeaFront } from '../seaFront/constants' -import type { ControlUnit } from '../../types/controlUnit' import type { GeoJSON } from '../../types/GeoJSON' +import type { LegacyControlUnit } from '../../types/legacyControlUnit' import type { MissionAction } from '../../types/missionAction' import type { Except } from 'type-fest' export namespace Mission { export interface Mission { closedBy?: string - controlUnits: ControlUnit.ControlUnit[] + controlUnits: LegacyControlUnit.LegacyControlUnit[] endDateTimeUtc?: string facade?: SeaFront geom?: GeoJSON.MultiPolygon @@ -81,7 +81,7 @@ export namespace Mission { export type MissionPointFeatureProperties = { color: string - controlUnits: ControlUnit.ControlUnit[] + controlUnits: LegacyControlUnit.LegacyControlUnit[] endDateTimeUtc: string // A 0 ou 1 number is required for WebGL to understand boolean isAirMission: number diff --git a/frontend/src/domain/shared_slices/DisplayedComponent.ts b/frontend/src/domain/shared_slices/DisplayedComponent.ts index c3107f0119..772d5cd298 100644 --- a/frontend/src/domain/shared_slices/DisplayedComponent.ts +++ b/frontend/src/domain/shared_slices/DisplayedComponent.ts @@ -7,28 +7,12 @@ import type { PayloadAction } from '@reduxjs/toolkit' const displayedComponentsLocalstorageKey = 'displayedComponents' -export type OptionalDisplayedComponentAction = { - areVesselsDisplayed?: boolean - isAlertsMapButtonDisplayed?: boolean - isBeaconMalfunctionsMapButtonDisplayed?: boolean - isDrawLayerModalDisplayed?: boolean - isFavoriteVesselsMapButtonDisplayed?: boolean - isInterestPointMapButtonDisplayed?: boolean - isMeasurementMapButtonDisplayed?: boolean - isMissionsLayerDisplayed?: boolean - isMissionsMapButtonDisplayed?: boolean - isVesselFiltersMapButtonDisplayed?: boolean - isVesselLabelsMapButtonDisplayed?: boolean - isVesselListDisplayed?: boolean - isVesselListModalDisplayed?: boolean - isVesselSearchDisplayed?: boolean - isVesselVisibilityMapButtonDisplayed?: boolean -} - export type DisplayedComponentState = { areVesselsDisplayed: boolean isAlertsMapButtonDisplayed: boolean isBeaconMalfunctionsMapButtonDisplayed: boolean + isControlUnitDialogDisplayed: boolean + isControlUnitListDialogDisplayed: boolean isDrawLayerModalDisplayed: boolean isFavoriteVesselsMapButtonDisplayed: boolean isInterestPointMapButtonDisplayed: boolean @@ -46,6 +30,8 @@ const INITIAL_STATE: DisplayedComponentState = { areVesselsDisplayed: true, isAlertsMapButtonDisplayed: true, isBeaconMalfunctionsMapButtonDisplayed: true, + isControlUnitDialogDisplayed: true, + isControlUnitListDialogDisplayed: true, isDrawLayerModalDisplayed: false, isFavoriteVesselsMapButtonDisplayed: true, isInterestPointMapButtonDisplayed: true, @@ -73,7 +59,7 @@ const displayedComponentSlice = createSlice({ initialState: INITIAL_STATE, name: 'displayedComponent', reducers: { - setDisplayedComponents(state, action: PayloadAction) { + setDisplayedComponents(state, action: PayloadAction>) { Object.keys(INITIAL_STATE).forEach(propertyKey => { const value = getValueOrDefault(action.payload[propertyKey], state[propertyKey]) @@ -91,10 +77,11 @@ const displayedComponentSlice = createSlice({ } }) -export const { setDisplayedComponents } = displayedComponentSlice.actions - +export const displayedComponentActions = displayedComponentSlice.actions export const displayedComponentReducer = displayedComponentSlice.reducer +export const { setDisplayedComponents } = displayedComponentActions + function getValueOrDefault(value, defaultValue) { if (value !== undefined) { return value diff --git a/frontend/src/domain/types/controlResource.ts b/frontend/src/domain/types/controlResource.ts deleted file mode 100644 index a8ee8166d4..0000000000 --- a/frontend/src/domain/types/controlResource.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ControlResource { - id: number - name: string -} diff --git a/frontend/src/domain/types/controlUnit.ts b/frontend/src/domain/types/controlUnit.ts deleted file mode 100644 index f67617ed91..0000000000 --- a/frontend/src/domain/types/controlUnit.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Mission } from '../entities/mission/types' - -import type { ControlResource } from './controlResource' -import type { Undefine } from '@mtes-mct/monitor-ui' -import type { Except } from 'type-fest' - -export namespace ControlUnit { - export interface ControlUnit { - administration: string - contact: string | undefined - id: number - isArchived: boolean - name: string - resources: ControlResource[] - } - - export type ControlUnitData = Except - - export type ControlUnitDraft = Omit, 'resources'> & Pick - - export type EngagedControlUnits = { - controlUnit: ControlUnit - missionSources: Mission.MissionSource[] - }[] -} diff --git a/frontend/src/domain/types/legacyControlUnit.ts b/frontend/src/domain/types/legacyControlUnit.ts new file mode 100644 index 0000000000..e95d8d20a4 --- /dev/null +++ b/frontend/src/domain/types/legacyControlUnit.ts @@ -0,0 +1,23 @@ +import type { Undefine } from '@mtes-mct/monitor-ui' +import type { Except } from 'type-fest' + +export namespace LegacyControlUnit { + export interface LegacyControlUnit { + administration: string + contact: string | undefined + id: number + isArchived: boolean + name: string + resources: LegacyControlUnitResource[] + } + + export type LegacyControlUnitData = Except + + export type LegacyControlUnitDraft = Omit, 'resources'> & + Pick + + export interface LegacyControlUnitResource { + id: number + name: string + } +} diff --git a/frontend/src/domain/types/missionAction.ts b/frontend/src/domain/types/missionAction.ts index 6bdfaa719f..5b8beb4472 100644 --- a/frontend/src/domain/types/missionAction.ts +++ b/frontend/src/domain/types/missionAction.ts @@ -1,4 +1,4 @@ -import type { ControlUnit } from './controlUnit' +import type { LegacyControlUnit } from './legacyControlUnit' export namespace MissionAction { export interface MissionAction { @@ -6,7 +6,7 @@ export namespace MissionAction { actionType: MissionActionType closedBy: string | undefined controlQualityComments: string | undefined - controlUnits: ControlUnit.ControlUnit[] + controlUnits: LegacyControlUnit.LegacyControlUnit[] districtCode: string | undefined diversion: boolean | undefined emitsAis: ControlCheck | undefined diff --git a/frontend/src/domain/types/reporting.ts b/frontend/src/domain/types/reporting.ts index 2ac05a9c36..f658be943f 100644 --- a/frontend/src/domain/types/reporting.ts +++ b/frontend/src/domain/types/reporting.ts @@ -1,7 +1,7 @@ import { SeaFrontGroup } from '../entities/seaFront/constants' -import type { ControlUnit } from './controlUnit' import type { Infraction } from './infraction' +import type { LegacyControlUnit } from './legacyControlUnit' import type { PendingAlertValue } from '../entities/alerts/types' import type { VesselIdentifier } from '../entities/vessel/types' @@ -65,7 +65,7 @@ export type CurrentAndArchivedReportingsOfSelectedVessel = { export type InfractionSuspicion = { authorContact: string | null authorTrigram: string | null - controlUnit: ControlUnit.ControlUnit | null + controlUnit: LegacyControlUnit.LegacyControlUnit | null controlUnitId: number | null description: string dml: string @@ -79,7 +79,7 @@ export type InfractionSuspicion = { export type Observation = { authorContact: string | null authorTrigram: string | null - controlUnit: ControlUnit.ControlUnit | null + controlUnit: LegacyControlUnit.LegacyControlUnit | null controlUnitId: number | null description: string reportingActor: string diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/AreaNote.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/AreaNote.tsx new file mode 100644 index 0000000000..f17b3131cb --- /dev/null +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/AreaNote.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components' + +import { Section } from './shared/Section' +import { TextareaForm } from './shared/TextareaForm' + +import type { ControlUnit } from '@mtes-mct/monitor-ui' + +type AreaNoteProps = { + controlUnit: ControlUnit.ControlUnit + onSubmit: (nextControlUnit: ControlUnit.ControlUnit) => any +} +export function AreaNote({ controlUnit, onSubmit }: AreaNoteProps) { + return ( +
+ Secteur d’intervention + + + +
+ ) +} + +const StyledSectionBody = styled(Section.Body)` + padding: 24px 32px; + + > div:not(:first-child) { + margin-top: 8px; + } +` diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/Form.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/Form.tsx new file mode 100644 index 0000000000..a8d9f7d120 --- /dev/null +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/Form.tsx @@ -0,0 +1,98 @@ +import { Accent, Button, FormikTextInput, Icon, IconButton, THEME, useKey } from '@mtes-mct/monitor-ui' +import { Formik } from 'formik' +import styled from 'styled-components' + +import { CONTROL_UNIT_CONTACT_FORM_SCHEMA } from './constants' +import { FormikNameSelect } from './FormikNameSelect' + +import type { ControlUnitContactFormValues } from './types' +import type { CSSProperties } from 'react' +import type { Promisable } from 'type-fest' + +export type FormProps = { + className?: string + initialValues: ControlUnitContactFormValues + onCancel: () => Promisable + onDelete?: () => Promisable + onSubmit: (controlUnitContactFormValues: ControlUnitContactFormValues) => void + style?: CSSProperties +} +export function Form({ className, initialValues, onCancel, onDelete, onSubmit, style }: FormProps) { + const key = useKey([initialValues]) + const isNew = !initialValues.id + + return ( + + {({ handleSubmit }) => ( +
+ {isNew ? 'Ajouter un contact' : 'Éditer un contact'} + + + + + + +
+ + +
+ {onDelete && ( + + )} +
+
+
+ )} +
+ ) +} + +const Title = styled.p` + background-color: ${p => p.theme.color.gainsboro}; + margin: 0 0 2px; + padding: 8px 16px; + /* TODO This should be the default height everywhere to have a consistent and exact height of 18px. */ + /* Monitor UI provides that value: https://github.com/MTES-MCT/monitor-ui/blob/main/src/GlobalStyle.ts#L76. */ + line-height: 1.3846; +` + +const StyledForm = styled.form` + background-color: ${p => p.theme.color.gainsboro}; + padding: 16px; + + > div:not(:first-child) { + margin-top: 16px; + } +` + +const ActionBar = styled.div` + display: flex; + justify-content: space-between; + + > div:first-child { + > .Element-Button:last-child { + margin-left: 8px; + } + } +` + +// TODO Add `borderColor` in Monitor UI. +const DeleteButton = styled(IconButton)` + border-color: ${p => p.theme.color.maximumRed}; + padding: 0 4px; +` diff --git a/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/FormikNameSelect.tsx b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/FormikNameSelect.tsx new file mode 100644 index 0000000000..aad2e2752c --- /dev/null +++ b/frontend/src/features/ControlUnit/components/ControlUnitDialog/ControlUnitContactList/FormikNameSelect.tsx @@ -0,0 +1,81 @@ +import { Accent, ControlUnit, FormikTextInput, Icon, IconButton, Select } from '@mtes-mct/monitor-ui' +import { useField } from 'formik' +import { useCallback, useEffect, useState } from 'react' +import styled from 'styled-components' + +import { + CONTROL_UNIT_CONTACT_PREDEFINED_NAMES, + SORTED_CONTROL_UNIT_CONTACT_PREDEFINED_NAMES_AS_OPTIONS +} from './constants' + +export function FormikNameSelect() { + const [field, meta, helpers] = useField('name') + + const [isCustomName, setIsCustomName] = useState( + !!field.value && !ControlUnit.ControlUnitContactPredefinedName[field.value] + ) + + const cancelCustomName = useCallback( + () => { + setIsCustomName(false) + helpers.setValue(undefined) + }, + + // We don't want to trigger infinite re-rendering since `helpers.setValue` changes after each rendering + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + + const handleChange = useCallback( + (nextName: string | undefined) => { + if (nextName === 'SWITCH_TO_CUSTOM_NAME') { + setIsCustomName(true) + + return + } + + helpers.setValue(nextName) + }, + + // We don't want to trigger infinite re-rendering since `helpers.setValue` changes after each rendering + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ) + + useEffect(() => { + if (!isCustomName && field.value && CONTROL_UNIT_CONTACT_PREDEFINED_NAMES.includes(field.value)) { + setIsCustomName(false) + } + }, [field.value, isCustomName]) + + return isCustomName ? ( + + + + + ) : ( +