From a4eb2226cbd792125a238c307b46958a7a882d0d Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 24 May 2024 16:40:49 +0200 Subject: [PATCH 01/10] feat: signalement v1 --- .env.sample | 2 +- README.md | 1 + components/bal/address-preview.tsx | 29 +- components/bal/habilitation-infos.tsx | 5 +- components/bal/numero-editor.tsx | 6 +- .../bal/numero-editor/select-parcelles.tsx | 16 +- components/bal/signalement-infos.tsx | 46 ++- components/bal/toponyme-editor.tsx | 6 +- .../base-locale-card-content.tsx | 16 + components/base-locale-card/index.tsx | 27 ++ components/bases-locales-list/index.tsx | 8 +- components/breadcrumbs.tsx | 48 +-- components/certification-button.tsx | 6 - components/delay-bar.tsx | 20 + components/map/editable-marker.tsx | 85 ++-- components/map/hooks/bounds.ts | 6 +- components/map/layers/cadastre.ts | 24 ++ components/map/map-marker.tsx | 101 +++-- components/map/map.tsx | 2 + .../numero/signalement-create-numero.tsx | 44 -- .../numero/signalement-delete-numero.tsx | 71 ---- .../numero/signalement-update-numero.tsx | 277 ------------- components/signalement/signalement-card.tsx | 41 -- .../signalement-diff/accordion-card.tsx | 79 ++++ .../signalement-numero-diff-card.tsx | 93 +++++ .../signalement-parcelle-diff.tsx | 83 ++++ .../signalement-position-diff.tsx | 161 ++++++++ .../signalement-toponyme-diff-card.tsx | 58 +++ .../signalement-voie-diff-card.tsx | 34 ++ .../signalement-diff/text-diff.tsx | 60 +++ .../numero/signalement-create-numero.tsx | 134 ++++++ .../numero/signalement-delete-numero.tsx | 117 ++++++ .../numero/signalement-update-numero.tsx | 162 ++++++++ .../signalement-form-buttons.tsx | 159 +++++++ .../signalement-form/signalement-form.tsx | 159 +++++++ .../toponyme/signalement-update-toponyme.tsx | 118 ++++++ .../voie/signalement-update-voie.tsx | 73 ++++ components/signalement/signalement-header.tsx | 72 ++++ .../signalement/signalement-list-item.tsx | 116 ++++++ components/signalement/signalement-list.tsx | 198 +++++---- .../signalement/signalement-type-badge.tsx | 41 ++ components/signalement/signalement-viewer.tsx | 162 -------- .../signalement-viewer-create-numero.tsx | 104 +++++ .../signalement-viewer-delete-numero.tsx | 96 +++++ .../signalement-viewer-update-numero.tsx | 130 ++++++ .../signalement-viewer/signalement-viewer.tsx | 112 +++++ .../signalement-viewer-update-toponyme.tsx | 103 +++++ .../voie/signalement-viewer-update-voie.tsx | 52 +++ .../toponyme/signalement-update-toponyme.tsx | 189 --------- .../voie/signalement-update-voie.tsx | 74 ---- components/trash/list/items-deleted-list.tsx | 4 +- .../restore-voie/list-numeros-deleted.tsx | 8 +- components/voie/numeros-list.tsx | 8 +- contexts/layout.tsx | 12 +- contexts/map.tsx | 30 ++ contexts/markers.tsx | 14 +- contexts/parcelles.tsx | 126 ++++-- contexts/signalement.tsx | 26 +- hooks/fuse.ts | 7 + hooks/useSignalementMapDiff.ts | 181 ++++++++ lib/openapi-api-bal/index.ts | 2 + .../models/UpdateSignalementDTO.ts | 22 + .../services/SignalementsService.ts | 35 ++ lib/openapi-signalement/index.ts | 23 +- lib/openapi-signalement/models/Author.ts | 4 +- .../models/{AuthorDTO.ts => AuthorInput.ts} | 4 +- .../models/ChangesRequested.ts | 16 - lib/openapi-signalement/models/Client.ts | 14 + .../{ObjectId.ts => CreateClientDTO.ts} | 3 +- .../models/CreateSignalementDTO.ts | 17 +- .../models/CreateSourceDTO.ts | 20 + .../models/DeleteNumeroChangesRequestedDTO.ts | 9 + .../models/ExistingLocation.ts | 1 + .../models/ExistingNumero.ts | 3 + .../models/ExistingToponyme.ts | 5 + .../models/ExistingVoie.ts | 1 + .../models/NumeroChangesRequestedDTO.ts | 17 + .../models/PaginatedSignalementsDTO.ts | 12 + lib/openapi-signalement/models/Position.ts | 1 - .../models/PositionCoordinatesDTO.ts | 10 + lib/openapi-signalement/models/PositionDTO.ts | 28 ++ lib/openapi-signalement/models/Signalement.ts | 30 +- .../models/SignalementStatsDTO.ts | 11 + lib/openapi-signalement/models/Source.ts | 25 ++ .../models/ToponymeChangesRequestedDTO.ts | 14 + .../models/UpdateSignalementDTO.ts | 14 +- .../models/VoieChangesRequestedDTO.ts | 10 + .../services/ClientsService.ts | 31 ++ .../services/DefaultService.ts | 67 --- .../services/SignalementsService.ts | 111 +++++ .../services/SourcesService.ts | 85 ++++ .../services/StatsService.ts | 25 ++ lib/utils/address.ts | 33 ++ lib/utils/date.ts | 18 + lib/utils/signalement.ts | 182 +++++++- package.json | 3 +- pages/_app.tsx | 18 + .../[balId]/signalements/[idSignalement].tsx | 388 ++++++++++-------- pages/bal/[balId]/signalements/index.tsx | 268 +++++++++--- pages/bal/[balId]/toponymes/[idToponyme].tsx | 8 +- pages/bal/[balId]/voies/[idVoie].tsx | 1 - yarn.lock | 5 + 102 files changed, 4312 insertions(+), 1524 deletions(-) create mode 100644 components/delay-bar.tsx delete mode 100644 components/signalement/numero/signalement-create-numero.tsx delete mode 100644 components/signalement/numero/signalement-delete-numero.tsx delete mode 100644 components/signalement/numero/signalement-update-numero.tsx delete mode 100644 components/signalement/signalement-card.tsx create mode 100644 components/signalement/signalement-diff/accordion-card.tsx create mode 100644 components/signalement/signalement-diff/signalement-numero-diff-card.tsx create mode 100644 components/signalement/signalement-diff/signalement-parcelle-diff.tsx create mode 100644 components/signalement/signalement-diff/signalement-position-diff.tsx create mode 100644 components/signalement/signalement-diff/signalement-toponyme-diff-card.tsx create mode 100644 components/signalement/signalement-diff/signalement-voie-diff-card.tsx create mode 100644 components/signalement/signalement-diff/text-diff.tsx create mode 100644 components/signalement/signalement-form/numero/signalement-create-numero.tsx create mode 100644 components/signalement/signalement-form/numero/signalement-delete-numero.tsx create mode 100644 components/signalement/signalement-form/numero/signalement-update-numero.tsx create mode 100644 components/signalement/signalement-form/signalement-form-buttons.tsx create mode 100644 components/signalement/signalement-form/signalement-form.tsx create mode 100644 components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx create mode 100644 components/signalement/signalement-form/voie/signalement-update-voie.tsx create mode 100644 components/signalement/signalement-header.tsx create mode 100644 components/signalement/signalement-list-item.tsx create mode 100644 components/signalement/signalement-type-badge.tsx delete mode 100644 components/signalement/signalement-viewer.tsx create mode 100644 components/signalement/signalement-viewer/numero/signalement-viewer-create-numero.tsx create mode 100644 components/signalement/signalement-viewer/numero/signalement-viewer-delete-numero.tsx create mode 100644 components/signalement/signalement-viewer/numero/signalement-viewer-update-numero.tsx create mode 100644 components/signalement/signalement-viewer/signalement-viewer.tsx create mode 100644 components/signalement/signalement-viewer/toponyme/signalement-viewer-update-toponyme.tsx create mode 100644 components/signalement/signalement-viewer/voie/signalement-viewer-update-voie.tsx delete mode 100644 components/signalement/toponyme/signalement-update-toponyme.tsx delete mode 100644 components/signalement/voie/signalement-update-voie.tsx create mode 100644 hooks/useSignalementMapDiff.ts create mode 100644 lib/openapi-api-bal/models/UpdateSignalementDTO.ts create mode 100644 lib/openapi-api-bal/services/SignalementsService.ts rename lib/openapi-signalement/models/{AuthorDTO.ts => AuthorInput.ts} (65%) delete mode 100644 lib/openapi-signalement/models/ChangesRequested.ts create mode 100644 lib/openapi-signalement/models/Client.ts rename lib/openapi-signalement/models/{ObjectId.ts => CreateClientDTO.ts} (73%) create mode 100644 lib/openapi-signalement/models/CreateSourceDTO.ts create mode 100644 lib/openapi-signalement/models/DeleteNumeroChangesRequestedDTO.ts create mode 100644 lib/openapi-signalement/models/NumeroChangesRequestedDTO.ts create mode 100644 lib/openapi-signalement/models/PaginatedSignalementsDTO.ts create mode 100644 lib/openapi-signalement/models/PositionCoordinatesDTO.ts create mode 100644 lib/openapi-signalement/models/PositionDTO.ts create mode 100644 lib/openapi-signalement/models/SignalementStatsDTO.ts create mode 100644 lib/openapi-signalement/models/Source.ts create mode 100644 lib/openapi-signalement/models/ToponymeChangesRequestedDTO.ts create mode 100644 lib/openapi-signalement/models/VoieChangesRequestedDTO.ts create mode 100644 lib/openapi-signalement/services/ClientsService.ts delete mode 100644 lib/openapi-signalement/services/DefaultService.ts create mode 100644 lib/openapi-signalement/services/SignalementsService.ts create mode 100644 lib/openapi-signalement/services/SourcesService.ts create mode 100644 lib/openapi-signalement/services/StatsService.ts create mode 100644 lib/utils/address.ts create mode 100644 lib/utils/date.ts diff --git a/.env.sample b/.env.sample index 6396ab4ec..33fdf3e3f 100644 --- a/.env.sample +++ b/.env.sample @@ -9,4 +9,4 @@ NEXT_PUBLIC_MATOMO_SITE_ID= NEXT_PUBLIC_MATOMO_TRACKER_URL=https://stats.beta.gouv.fr/ NEXT_PUBLIC_BAL_ADMIN_URL= NEXT_PUBLIC_API_SIGNALEMENT= -PORT=3000 \ No newline at end of file +PORT=3000 diff --git a/README.md b/README.md index 4cc325564..a380f06d6 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Elles peuvent être définies classiquement ou en créant un fichier `.env` sur | `NEXT_PUBLIC_PEERTUBE` | URL du peertube | | `NEXT_PUBLIC_MATOMO_TRACKER_URL` | URL du matomo | | `NEXT_PUBLIC_MATOMO_SITE_ID` | Id du site sur matomo | +| `NEXT_PUBLIC_API_SIGNALEMENT` | URL de l'API signalement | | `NEXT_PUBLIC_BAL_ADMIN_URL` | URL de base de bal admin | | `PORT` | Port de l'application | diff --git a/components/bal/address-preview.tsx b/components/bal/address-preview.tsx index 90a8d4c2b..ac0dbe0ba 100644 --- a/components/bal/address-preview.tsx +++ b/components/bal/address-preview.tsx @@ -1,31 +1,14 @@ import { Pane, Text } from "evergreen-ui"; -import { computeCompletNumero } from "@/lib/utils/numero"; import { CommuneType } from "@/types/commune"; - -const getAddressPreview = (numero, suffixe, toponyme, voie, commune) => { - const completNumero = computeCompletNumero(numero, suffixe) || ""; - if (toponyme) { - return `${completNumero} ${voie}, ${toponyme} - ${commune.nom} (${commune.code})`; - } - - if (voie) { - return `${completNumero} ${voie} - ${commune.nom} (${commune.code})`; - } - - if (!voie && !toponyme) { - return `${completNumero} - ${commune.nom} (${commune.code})`; - } - - return `${completNumero} ${voie} - ${commune.nom} (${commune.code})`; -}; +import { getAddressPreview } from "@/lib/utils/address"; interface AddressPreviewProps { numero: string | number; suffixe?: string; - selectedNomToponyme: string; - voie: string; - commune: CommuneType; + selectedNomToponyme?: string; + voie?: string; + commune?: CommuneType; } function AddressPreview({ @@ -38,9 +21,9 @@ function AddressPreview({ const address = getAddressPreview( numero, suffixe, + commune, selectedNomToponyme, - voie, - commune + voie ); return ( diff --git a/components/bal/habilitation-infos.tsx b/components/bal/habilitation-infos.tsx index 2689ebc15..c20d94cd4 100644 --- a/components/bal/habilitation-infos.tsx +++ b/components/bal/habilitation-infos.tsx @@ -61,9 +61,8 @@ function HabilitationInfos({ commune }: HabilitationInfosProps) { } > - Cette Base Adresse Locale détient une habilitation permettant - d'alimenter la Base Adresse Nationale pour la commune de{" "} - {commune.nom}. Cette habilitation est valide jusqu'au{" "} + Toutes les modifications remonteront automatiquement dans la Base + Adresse Nationale jusqu'au{" "} {format(new Date(habilitation.expiresAt), "dd/MM/yyyy")}. diff --git a/components/bal/numero-editor.tsx b/components/bal/numero-editor.tsx index 29c621f76..05d06c728 100644 --- a/components/bal/numero-editor.tsx +++ b/components/bal/numero-editor.tsx @@ -85,7 +85,7 @@ function NumeroEditor({ refreshBALSync, reloadVoies, } = useContext(BalDataContext); - const { selectedParcelles } = useContext(ParcellesContext); + const { highlightedParcelles } = useContext(ParcellesContext); const { markers, suggestedNumero, setCompleteNumero } = useContext(MarkersContext); const { reloadTiles } = useContext(MapContext); @@ -116,7 +116,7 @@ function NumeroEditor({ numero: Number(numero), suffixe: suffixe?.length > 0 ? suffixe.toLowerCase().trim() : null, comment: comment.length > 0 ? comment : null, - parcelles: selectedParcelles, + parcelles: highlightedParcelles, certifie: certifie ?? (initialValue?.certifie || false), }; @@ -143,7 +143,7 @@ function NumeroEditor({ certifie, toponymeId, comment, - selectedParcelles, + highlightedParcelles, ]); const onFormSubmit = useCallback( diff --git a/components/bal/numero-editor/select-parcelles.tsx b/components/bal/numero-editor/select-parcelles.tsx index 518868fcc..253429f47 100644 --- a/components/bal/numero-editor/select-parcelles.tsx +++ b/components/bal/numero-editor/select-parcelles.tsx @@ -26,8 +26,8 @@ function SelectParcelles({ const { isCadastreDisplayed, setIsCadastreDisplayed } = useContext(MapContext); const { - selectedParcelles, - setSelectedParcelles, + highlightedParcelles, + setHighlightedParcelles, setIsParcelleSelectionEnabled, hoveredParcelle, handleHoveredParcelle, @@ -36,13 +36,17 @@ function SelectParcelles({ const addressType = isToponyme ? "toponyme" : "numéro"; useEffect(() => { - setSelectedParcelles(initialParcelles); + setHighlightedParcelles(initialParcelles); setIsParcelleSelectionEnabled(true); return () => { setIsParcelleSelectionEnabled(false); }; - }, []); + }, [ + setHighlightedParcelles, + setIsParcelleSelectionEnabled, + initialParcelles, + ]); return ( @@ -50,9 +54,9 @@ function SelectParcelles({ title="Parcelles cadastre" help={`Depuis la carte, cliquez sur les parcelles que vous souhaitez ajouter au ${addressType}. En précisant les parcelles associées à cette adresse, vous accélérez sa réutilisation par de nombreux services, DDFiP, opérateurs de courrier, de fibre et de GPS.`} /> - {selectedParcelles.length > 0 ? ( + {highlightedParcelles.length > 0 ? ( - {selectedParcelles.map((parcelle) => { + {highlightedParcelles.map((parcelle) => { const isHovered = parcelle === hoveredParcelle?.id; return ( diff --git a/components/bal/signalement-infos.tsx b/components/bal/signalement-infos.tsx index cd0825d95..cb611e47d 100644 --- a/components/bal/signalement-infos.tsx +++ b/components/bal/signalement-infos.tsx @@ -1,7 +1,7 @@ import React from "react"; -import NextLink from "next/link"; -import { Pane, Text, Heading, Link } from "evergreen-ui"; +import { Pane, Heading, Button, Alert } from "evergreen-ui"; import { Signalement } from "@/lib/openapi-signalement"; +import { useRouter } from "next/navigation"; interface SignalementInfosProps { balId: string; @@ -9,17 +9,41 @@ interface SignalementInfosProps { } function SignalementInfos({ balId, signalements }: SignalementInfosProps) { + const router = useRouter(); + const onClick = () => { + router.push(`/bal/${balId}/signalements`); + }; + return ( - + Signalements - - Vous avez {signalements.length}{" "} - {signalements.length > 1 ? "signalements" : "signalement"} en attente de - traitement. - - - Consulter les signalements - + + Vous avez reçu {signalements.length}{" "} + {signalements.length > 1 ? "propositions" : "proposition"}. + + } + > + + ); } diff --git a/components/bal/toponyme-editor.tsx b/components/bal/toponyme-editor.tsx index bf31f1dd3..d05718e96 100644 --- a/components/bal/toponyme-editor.tsx +++ b/components/bal/toponyme-editor.tsx @@ -60,7 +60,7 @@ function ToponymeEditor({ reloadNumeros, } = useContext(BalDataContext); const { markers } = useContext(MarkersContext); - const { selectedParcelles } = useContext(ParcellesContext); + const { highlightedParcelles } = useContext(ParcellesContext); const [ref, setIsFocus] = useFocus(true); const updateNumerosToponyme = useCallback( @@ -89,7 +89,7 @@ function ToponymeEditor({ nom, nomAlt: Object.keys(nomAlt).length > 0 ? nomAlt : null, positions: [], - parcelles: selectedParcelles, + parcelles: highlightedParcelles, }; if (markers) { @@ -163,7 +163,7 @@ function ToponymeEditor({ nom, nomAlt, markers, - selectedParcelles, + highlightedParcelles, setToponyme, closeForm, refreshBALSync, diff --git a/components/base-locale-card/base-locale-card-content.tsx b/components/base-locale-card/base-locale-card-content.tsx index 384c555f6..ded6292b1 100644 --- a/components/base-locale-card/base-locale-card-content.tsx +++ b/components/base-locale-card/base-locale-card-content.tsx @@ -38,6 +38,7 @@ interface BaseLocaleCardContentProps { onRemove?: (e: any) => void; onHide?: (e: any) => void; isShownHabilitationStatus?: boolean; + pendingSignalementsCount?: number; } function BaseLocaleCardContent({ @@ -50,6 +51,7 @@ function BaseLocaleCardContent({ onSelect, onRemove, onHide, + pendingSignalementsCount, }: BaseLocaleCardContentProps) { const { status, createdAt, emails } = baseLocale; const { isMobile } = useContext(LayoutContext); @@ -120,6 +122,20 @@ function BaseLocaleCardContent({ )} + {Boolean(pendingSignalementsCount) && ( + + Signalements + + {pendingSignalementsCount} + + + )} + {commune && ( ( null ); + const [pendingSignalementsCount, setPendingSignalementsCount] = + useState(); const [isHabilitationValid, setIsHabilitationValid] = useState(false); const [isOpen, setIsOpen] = useState(isAdmin ? isDefaultOpen : false); @@ -100,11 +104,33 @@ function BaseLocaleCard({ Object.assign(OpenAPI, { TOKEN: null }); }; + const fetchPendingSignalementsCount = async () => { + const paginatedSignalements = await SignalementsService.getSignalements( + 1, + undefined, + [Signalement.status.PENDING], + undefined, + undefined, + [baseLocale.commune] + ); + setPendingSignalementsCount(paginatedSignalements.total); + }; + void fetchCommune(); if (!baseLocale.token) { void fetchHabilitationIsValid(); } else { + const signalementWhiteList = + process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST?.split(",") || + []; + if ( + baseLocale.status === BaseLocale.status.PUBLISHED && + process.env.NEXT_PUBLIC_API_SIGNALEMENT && + signalementWhiteList.includes(baseLocale.commune) + ) { + void fetchPendingSignalementsCount(); + } void fetchHabilitation(); } }, [baseLocale]); @@ -181,6 +207,7 @@ function BaseLocaleCard({ onSelect={onSelect} onRemove={onRemove} onHide={onHide} + pendingSignalementsCount={pendingSignalementsCount} /> )} diff --git a/components/bases-locales-list/index.tsx b/components/bases-locales-list/index.tsx index 5ea1442f8..434ab4a70 100644 --- a/components/bases-locales-list/index.tsx +++ b/components/bases-locales-list/index.tsx @@ -16,6 +16,10 @@ interface BasesLocalesListProps { basesLocales: ExtendedBaseLocaleDTO[]; } +const fuseOptions = { + keys: ["nom", "commune"], +}; + function BasesLocalesList({ basesLocales }: BasesLocalesListProps) { const { removeBAL, getHiddenBal, addHiddenBal } = useContext(LocalStorageContext); @@ -33,9 +37,7 @@ function BasesLocalesList({ basesLocales }: BasesLocalesListProps) { Router.push(`/bal/${bal.id}`); }; - const [filtered, onFilter] = useFuse(basesLocales, 200, { - keys: ["nom", "commune"], - }); + const [filtered, onFilter] = useFuse(basesLocales, 200, fuseOptions); const onRemove = useCallback(async () => { await removeBAL(toRemove); diff --git a/components/breadcrumbs.tsx b/components/breadcrumbs.tsx index 3a8d4e90a..0594d3359 100644 --- a/components/breadcrumbs.tsx +++ b/components/breadcrumbs.tsx @@ -5,8 +5,7 @@ import { CommuneType } from "@/types/commune"; import { BaseLocale, Toponyme, Voie } from "@/lib/openapi-api-bal"; import { useRouter } from "next/router"; import { capitalize } from "lodash"; -import SignalementContext from "@/contexts/signalement"; -import { getSignalementLabel } from "@/lib/utils/signalement"; +import LayoutContext from "@/contexts/layout"; type BreadcrumbsProps = { baseLocale: BaseLocale; @@ -24,7 +23,7 @@ function Breadcrumbs({ ...props }: BreadcrumbsProps) { const router = useRouter(); - const { signalements } = useContext(SignalementContext); + const { breadcrumbs } = useContext(LayoutContext); const balEditorPath = router.pathname.split("[balId]")[1]; const innerPathSplitted = balEditorPath?.split("/"); @@ -40,15 +39,6 @@ function Breadcrumbs({ secondInnerPathLabel = voie?.nom || toponyme?.nom; } - let signalementLabel; - if (innerPath === "signalements" && router.query.idSignalement) { - const signalementId = router.query.idSignalement; - const signalement = signalements.find( - (signalement) => signalement._id === signalementId - ); - signalementLabel = signalement && getSignalementLabel(signalement); - } - return ( {baseLocale.nom || commune.nom}} - {innerPath && ( + {breadcrumbs ? ( + breadcrumbs + ) : innerPath ? ( <> {baseLocale.nom || commune.nom} {" > "} - {signalementLabel ? ( + {secondInnerPathLabel ? ( <> - + {innerPathLabel} + {" > "} - {signalementLabel} + {secondInnerPathLabel} ) : ( - <> - {secondInnerPathLabel ? ( - <> - - {innerPathLabel} - - - {" > "} - {secondInnerPathLabel} - - ) : ( - {innerPathLabel} - )} - + {innerPathLabel} )} - )} + ) : null} ); } diff --git a/components/certification-button.tsx b/components/certification-button.tsx index 08fd3ef80..d7fa245a7 100644 --- a/components/certification-button.tsx +++ b/components/certification-button.tsx @@ -82,18 +82,12 @@ function CertificationButton({ position: sticky; width: 100%; bottom: -12px; - // bottom: 0; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; padding: 10px 0; - background-color: #e6e8f0; - - // background-color: white; - // border-radius: 8px; - // border: 1px solid #c1c4d6; } .certification-button-wrapper > div { diff --git a/components/delay-bar.tsx b/components/delay-bar.tsx new file mode 100644 index 000000000..bd6e8b03e --- /dev/null +++ b/components/delay-bar.tsx @@ -0,0 +1,20 @@ +import { Pane } from "evergreen-ui"; + +interface DelayBarProps { + delay: string; +} + +export function DelayBar({ delay }: DelayBarProps) { + return ( + + + + ); +} diff --git a/components/map/editable-marker.tsx b/components/map/editable-marker.tsx index 9b64da204..668e95d92 100644 --- a/components/map/editable-marker.tsx +++ b/components/map/editable-marker.tsx @@ -1,11 +1,10 @@ -import { useState, useCallback, useContext, useEffect, useMemo } from "react"; +import { useCallback, useContext, useEffect, useMemo } from "react"; import { Marker, ViewState } from "react-map-gl"; import { Pane, MapMarkerIcon, Text } from "evergreen-ui"; import nearestPointOnLine from "@turf/nearest-point-on-line"; import length from "@turf/length"; import * as helpers from "@turf/helpers"; import lineSlice from "@turf/line-slice"; -import { Coord } from "@turf/helpers"; import MapContext from "@/contexts/map"; import MarkersContext from "@/contexts/markers"; @@ -94,47 +93,49 @@ function EditableMarker({ computeSuggestedNumero, ]); - return markers.map((marker, idx) => ( - onDrag(e, idx)} - onDragEnd={(e) => onDrag(e, idx)} - > - - - {completeNumero - ? `${completeNumero} - ${marker.type}` - : `${marker.type}`} - + return markers + .filter((marker) => !marker.isMapMarker) + .map((marker, idx) => ( + onDrag(e, idx)} + onDragEnd={(e) => onDrag(e, idx)} + > + + + {completeNumero + ? `${completeNumero} - ${marker.type}` + : `${marker.type}`} + - - - - )); + + + + )); } export default EditableMarker; diff --git a/components/map/hooks/bounds.ts b/components/map/hooks/bounds.ts index 6c00e6d0e..abb681a55 100644 --- a/components/map/hooks/bounds.ts +++ b/components/map/hooks/bounds.ts @@ -1,13 +1,11 @@ import { useMemo, useContext, useState, useEffect, useCallback } from "react"; import bbox from "@turf/bbox"; -import type { LngLatBoundsLike, Map, VectorTileSource } from "maplibre-gl"; +import type { Map } from "maplibre-gl"; import BalDataContext from "@/contexts/bal-data"; -import MapContext from "@/contexts/map"; -import { NextRouter, useRouter } from "next/router"; +import { NextRouter } from "next/router"; import { CommuneType } from "@/types/commune"; import { Toponyme, Voie } from "@/lib/openapi-api-bal"; -import { ViewState } from "react-map-gl"; function useBounds( map: Map, diff --git a/components/map/layers/cadastre.ts b/components/map/layers/cadastre.ts index 62a2db1cb..f72b7d57f 100644 --- a/components/map/layers/cadastre.ts +++ b/components/map/layers/cadastre.ts @@ -1,3 +1,5 @@ +import { SignalementDiff } from "@/lib/utils/signalement"; + export const SOURCE = "cadastre"; export const SOURCE_LAYER = { @@ -13,6 +15,7 @@ export const LAYER = { PARCELLES_FILL: "parcelles-fill", PARCELLES_SELECTED: "parcelles-selected", PARCELLE_HIGHLIGHTED: "parcelle-highlighted", + PARCELLE_HIGHLIGHTED_DIFF_MODE: "parcelle-highlighted-diff-mode", SECTIONS: "sections", CODE_SECTION: "code-section", CODE_PARCELLES: "code-parcelles", @@ -120,6 +123,27 @@ export const cadastreLayers = [ "fill-opacity": 0.7, }, }, + { + id: LAYER.PARCELLE_HIGHLIGHTED_DIFF_MODE, + type: "fill", + source: SOURCE, + "source-layer": SOURCE_LAYER.PARCELLES, + layout: { + visibility: "none", + }, + filter: ["==", "id", ""], + paint: { + "fill-color": [ + "case", + ["==", ["feature-state", "diff"], SignalementDiff.DELETED], + "rgba(244, 228, 219, 1)", + ["==", ["feature-state", "diff"], SignalementDiff.NEW], + "rgba(218, 244, 246, 1)", + "rgba(200, 200, 200, 1)", + ], + "fill-opacity": 0.7, + }, + }, { id: LAYER.SECTIONS, type: "line", diff --git a/components/map/map-marker.tsx b/components/map/map-marker.tsx index 12345bfbd..a159abd2f 100644 --- a/components/map/map-marker.tsx +++ b/components/map/map-marker.tsx @@ -1,5 +1,5 @@ -import React from "react"; -import { Marker } from "react-map-gl"; +import React, { useEffect, useRef, useState } from "react"; +import { Marker, Popup } from "react-map-gl/maplibre"; import { MapMarkerIcon, Pane, Text } from "evergreen-ui"; import { Marker as MarkerType } from "contexts/markers"; @@ -9,38 +9,87 @@ interface MapMarkerProps { } function MapMarker({ size = 32, marker }: MapMarkerProps) { - const { id, label, color, latitude, longitude, onClick } = marker; + const markerRef = useRef(null); + const { + id, + tooltip, + color, + latitude, + longitude, + onClick, + showTooltip, + label, + } = marker; + + const [_showTooltip, setShowTooltip] = useState(false); + + useEffect(() => { + const element = markerRef.current.getElement(); + if (element) { + const onMouseEnter = () => { + setShowTooltip(true); + }; + const onMouseLeave = () => { + setShowTooltip(false); + }; + + element.addEventListener("mouseenter", onMouseEnter); + element.addEventListener("mouseleave", onMouseLeave); + + return () => { + element.removeEventListener("mouseenter", onMouseEnter); + element.removeEventListener("mouseleave", onMouseLeave); + }; + } + }, [markerRef]); + return ( - - - + {!!tooltip && (showTooltip || _showTooltip) && ( + - {label} - + {tooltip} + + )} + + {label && ( + + + {label} + + + )} - - + + ); } diff --git a/components/map/map.tsx b/components/map/map.tsx index 6685755e2..f32a1e8aa 100644 --- a/components/map/map.tsx +++ b/components/map/map.tsx @@ -114,6 +114,7 @@ function Map({ commune, isAddressFormOpen, handleAddressForm }: MapProps) { setIsCadastreDisplayed, balTilesUrl, isMapLoaded, + showToponymes, } = useContext(MapContext); const { isParcelleSelectionEnabled, handleParcelle } = useContext(ParcellesContext); @@ -433,6 +434,7 @@ function Map({ commune, isAddressFormOpen, handleAddressForm }: MapProps) { )} {toponymes && + showToponymes && viewport.zoom > TOPONYMES_MIN_ZOOM && toponymes.map((toponyme) => ( Promise; - handleClose: () => void; - commune: CommuneType; -} - -function SignalementCreateNumero({ - signalement, - initialVoieId, - handleSubmit, - handleClose, - commune, -}: SignalementCreateNumeroProps) { - return ( - - - Ignorer - - ), - }} - /> - - ); -} - -export default SignalementCreateNumero; diff --git a/components/signalement/numero/signalement-delete-numero.tsx b/components/signalement/numero/signalement-delete-numero.tsx deleted file mode 100644 index f901337cb..000000000 --- a/components/signalement/numero/signalement-delete-numero.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Button, Pane } from "evergreen-ui"; -import React, { useContext } from "react"; -import NumeroEditor from "../../bal/numero-editor"; -import { CommuneType } from "@/types/commune"; -import BalDataContext from "@/contexts/bal-data"; -import MapContext from "@/contexts/map"; -import { Numero, NumerosService, Voie } from "@/lib/openapi-api-bal"; -import LayoutContext from "@/contexts/layout"; - -interface SignalementDeleteNumeroProps { - existingLocation: Numero & { voie: Voie }; - handleSubmit: () => Promise; - handleClose: () => void; - commune: CommuneType; -} - -function SignalementDeleteNumero({ - existingLocation, - handleSubmit, - handleClose, - commune, -}: SignalementDeleteNumeroProps) { - const { reloadNumeros, reloadParcelles, refreshBALSync } = - useContext(BalDataContext); - const { reloadTiles } = useContext(MapContext); - const { toaster } = useContext(LayoutContext); - - const handleRemove = async (idNumero) => { - const softDeleteNumero = toaster( - async () => { - await NumerosService.softDeleteNumero(idNumero); - await reloadNumeros(); - await reloadParcelles(); - reloadTiles(); - refreshBALSync(); - }, - "Le numéro a bien été archivé", - "Le numéro n’a pas pu être archivé" - ); - await softDeleteNumero(); - await handleSubmit(); - }; - - return ( - - handleRemove(existingLocation.id)} - > - Supprimer - - ), - }} - /> - - ); -} - -export default SignalementDeleteNumero; diff --git a/components/signalement/numero/signalement-update-numero.tsx b/components/signalement/numero/signalement-update-numero.tsx deleted file mode 100644 index 5f34d1ea2..000000000 --- a/components/signalement/numero/signalement-update-numero.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { Alert, Badge, Button, Pane, Paragraph, Strong } from "evergreen-ui"; -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import ReactDOM from "react-dom"; -import NumeroEditor from "../../bal/numero-editor"; -import { CommuneType } from "@/types/commune"; -import SignalementCard from "../signalement-card"; -import MarkersContext from "@/contexts/markers"; -import PositionItem from "../../bal/position-item"; -import { Numero, Position, Voie, VoiesService } from "@/lib/openapi-api-bal"; -import { - Signalement, - Position as PositionSignalement, -} from "@/lib/openapi-signalement"; -import LayoutContext from "@/contexts/layout"; - -interface SignalementUpdateNumeroProps { - signalement: Signalement; - existingLocation: Numero & { voie: Voie }; - handleSubmit: () => Promise; - handleClose: () => void; - commune: CommuneType; -} - -const detectChanges = (signalement, existingLocation) => { - const { numero, suffixe, positions, parcelles, nomVoie } = - signalement.changesRequested; - - const numeroComplet = `${numero}${suffixe ? suffixe : ""}`; - - const { - numeroComplet: existingNumeroComplet, - positions: existingPositions, - parcelles: existingParcelles, - voie: existingVoie, - } = existingLocation; - - return { - voie: nomVoie !== existingVoie?.nom, - numero: numeroComplet !== existingNumeroComplet, - positions: - JSON.stringify(positions.map(({ point, type }) => ({ point, type }))) !== - JSON.stringify( - existingPositions.map(({ point, type }) => ({ point, type })) - ), - parcelles: JSON.stringify(parcelles) !== JSON.stringify(existingParcelles), - }; -}; - -function SignalementUpdateNumero({ - signalement, - existingLocation, - handleSubmit, - handleClose, - commune, -}: SignalementUpdateNumeroProps) { - const voieInputRef = useRef(null); - const numeroInputRef = useRef(null); - const positionsInputRef = useRef(null); - const parcellesInputRef = useRef(null); - - const refs = useMemo( - () => ({ - voie: voieInputRef, - numero: numeroInputRef, - positions: positionsInputRef, - parcelles: parcellesInputRef, - }), - [] - ); - - const { markers, addMarker, removeMarker, disableMarkers } = - useContext(MarkersContext); - - const { pushToast } = useContext(LayoutContext); - - const [changes, setChanges] = useState( - detectChanges(signalement, existingLocation) - ); - const [refsInitialized, setRefsInitialized] = useState(false); - const [voieWillBeRenamed, setVoieWillBeRenamed] = useState(false); - const [numeroEditorValue, setNumeroEditorValue] = useState(existingLocation); - - const { numero, suffixe, positions, parcelles, nomVoie } = - signalement.changesRequested; - - useEffect(() => { - const refKeys = Object.keys(refs); - refKeys.forEach((key) => { - if (refs[key].current) { - changes[key] - ? (refs[key].current.style.border = "solid #f3b346 2px") - : (refs[key].current.style.border = "none"); - } - }); - if (refKeys.every((key) => Boolean(refs[key].current))) { - setRefsInitialized(true); - } - }, [refs, changes]); - - useEffect(() => { - if (positions) { - positions.forEach((position: PositionSignalement & { _id: string }) => { - changes.positions - ? addMarker({ - id: position._id, - isMapMarker: true, - isDisabled: true, - color: "warning", - label: `${position.type} - ${numero}${suffixe ? suffixe : ""}`, - longitude: position.point.coordinates[0], - latitude: position.point.coordinates[1], - type: position.type as unknown as Position.type, - }) - : removeMarker(position._id); - }); - } - - return () => { - disableMarkers(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [positions, changes.positions]); - - const handleAcceptChange = (key: string, value: any) => { - if (key === "numero") { - setNumeroEditorValue({ - ...numeroEditorValue, - numero: value[0], - suffixe: value[1], - }); - } else { - setNumeroEditorValue({ ...numeroEditorValue, [key]: value }); - } - - setChanges({ ...changes, [key]: false }); - }; - - const handleRefuseChange = (key: string) => { - setChanges({ ...changes, [key]: false }); - }; - - const onSubmitted = useCallback(async () => { - if (voieWillBeRenamed) { - try { - await VoiesService.updateVoie(existingLocation.voie.id, { - nom: nomVoie, - }); - } catch (e) { - pushToast({ - title: "Le renommage de la voie a échoué", - intent: "danger", - }); - console.error(e); - } - } - await handleSubmit(); - }, [voieWillBeRenamed, handleSubmit, existingLocation, nomVoie, pushToast]); - - return ( - - handleRefuseChange("voie")} - refs={refs} - /> - {refsInitialized && - changes.voie && - ReactDOM.createPortal( - voieWillBeRenamed ? ( - - - - Après l'enregistrement, la voie " - {existingLocation.voie.nom}" sera renommée en " - {nomVoie}". - - - - - - - ) : ( - setVoieWillBeRenamed(true)} - onRefuse={() => handleRefuseChange("voie")} - > - {nomVoie} - - ), - refs.voie.current - )} - {refsInitialized && - changes.numero && - ReactDOM.createPortal( - handleAcceptChange("numero", [numero, suffixe])} - onRefuse={() => handleRefuseChange("numero")} - > - - {numero} {suffixe} - - , - refs.numero.current - )} - {refsInitialized && - changes.positions && - ReactDOM.createPortal( - handleAcceptChange("positions", positions)} - onRefuse={() => handleRefuseChange("positions")} - > - - - Type - -
- Latitude - Longitude -
- - {markers - .filter(({ isMapMarker }) => isMapMarker) - .map((marker) => ( - - ))} - - , - refs.positions.current - )} - {refsInitialized && - changes.parcelles && - ReactDOM.createPortal( - handleAcceptChange("parcelles", parcelles)} - onRefuse={() => handleRefuseChange("parcelles")} - > - {parcelles.map((parcelle) => ( - - {parcelle} - - ))} - , - refs.parcelles.current - )} - - ); -} - -export default SignalementUpdateNumero; diff --git a/components/signalement/signalement-card.tsx b/components/signalement/signalement-card.tsx deleted file mode 100644 index 64f9c96b8..000000000 --- a/components/signalement/signalement-card.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Alert, Button, Pane } from "evergreen-ui"; - -interface SignalementCardProps { - onAccept: () => void; - onRefuse: () => void; - children: React.ReactNode; -} - -function SignalementCard({ - onAccept, - onRefuse, - children, -}: SignalementCardProps) { - return ( - - - {children} - - - - - - - ); -} - -export default SignalementCard; diff --git a/components/signalement/signalement-diff/accordion-card.tsx b/components/signalement/signalement-diff/accordion-card.tsx new file mode 100644 index 000000000..7f5bccd62 --- /dev/null +++ b/components/signalement/signalement-diff/accordion-card.tsx @@ -0,0 +1,79 @@ +import { + CaretDownIcon, + CaretRightIcon, + Heading, + Icon, + Pane, +} from "evergreen-ui"; +import TextDiff from "./text-diff"; +import { SignalementPositionDiff } from "./signalement-position-diff"; +import { SignalementParcelleDiff } from "./signalement-parcelle-diff"; +import { useRef } from "react"; + +interface AccordionCardProps { + title: string; + backgroundColor?: string; + isActive?: boolean; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + children: React.ReactNode; +} + +export function AccordionCard({ + title, + backgroundColor, + isActive, + onMouseEnter, + onMouseLeave, + children, +}: AccordionCardProps) { + const contentRef = useRef(null); + + const handleMouseEnter = () => { + onMouseEnter && onMouseEnter(); + }; + + const handleMouseLeave = () => { + onMouseLeave && onMouseLeave(); + }; + + return ( + + + + {title} + + {onMouseEnter && ( + + )} + + + {children} + + + ); +} diff --git a/components/signalement/signalement-diff/signalement-numero-diff-card.tsx b/components/signalement/signalement-diff/signalement-numero-diff-card.tsx new file mode 100644 index 000000000..c287ea73f --- /dev/null +++ b/components/signalement/signalement-diff/signalement-numero-diff-card.tsx @@ -0,0 +1,93 @@ +import { Pane, Text } from "evergreen-ui"; +import TextDiff from "./text-diff"; +import { SignalementPositionDiff } from "./signalement-position-diff"; +import { SignalementParcelleDiff } from "./signalement-parcelle-diff"; +import { AccordionCard } from "./accordion-card"; + +interface SignalementNumeroDiffCardProps { + title: string; + numero: { + from?: string; + to: string; + }; + voie: { + from?: string; + to: string; + }; + positions: { + from?: any[]; + to: any[]; + }; + parcelles: { + from?: string[]; + to: string[]; + }; + complement: { + from?: string; + to: string; + }; + onMouseEnter?: () => void; + onMouseLeave?: () => void; + backgroundColor?: string; + isActive?: boolean; +} + +export function SignalementNumeroDiffCard({ + title, + numero, + voie, + positions, + parcelles, + complement, + onMouseEnter, + onMouseLeave, + backgroundColor, + isActive, +}: SignalementNumeroDiffCardProps) { + return ( + + + + + Numéro + + + + + + Voie + + + + + {complement.to && ( + + + Complément + + + + )} + + + + ); +} diff --git a/components/signalement/signalement-diff/signalement-parcelle-diff.tsx b/components/signalement/signalement-diff/signalement-parcelle-diff.tsx new file mode 100644 index 000000000..c490e21fe --- /dev/null +++ b/components/signalement/signalement-diff/signalement-parcelle-diff.tsx @@ -0,0 +1,83 @@ +import { SignalementDiff } from "@/lib/utils/signalement"; +import { Badge, Pane, PlusIcon, MinusIcon, Text } from "evergreen-ui"; + +interface SignalementParcelleDiffProps { + parcelles: string[]; + existingParcelles?: string[]; +} + +export const parcelleDiff = ( + parcelles: string[], + existingParcelles?: string[] +): { parcelle: string; diff: SignalementDiff }[] => { + const deletedParcelles = (existingParcelles || []) + .filter((existingParcelle) => !parcelles.includes(existingParcelle)) + .map((parcelle) => ({ + parcelle, + diff: SignalementDiff.DELETED, + })); + + const newOrUnchangedParcelles = parcelles.map((parcelle) => + !existingParcelles?.includes(parcelle) + ? { + parcelle, + diff: SignalementDiff.NEW, + } + : { + parcelle, + diff: SignalementDiff.UNCHANGED, + } + ); + + return [...deletedParcelles, ...newOrUnchangedParcelles]; +}; + +export function SignalementParcelleDiff({ + parcelles, + existingParcelles, +}: SignalementParcelleDiffProps) { + const showDiff = !!existingParcelles; + const isPlural = showDiff + ? parcelleDiff(parcelles, existingParcelles).length > 1 + : parcelles.length > 1; + + return parcelles.length > 0 ? ( + + + Parcelle{isPlural ? "s" : ""} + + + {showDiff ? ( + + {parcelleDiff(parcelles, existingParcelles).map( + ({ parcelle, diff }) => ( + + {diff === SignalementDiff.DELETED && ( + + )} + {diff === SignalementDiff.NEW && } + {parcelle} + + ) + )} + + ) : ( + parcelles.map((parcelle) => ( + + {parcelle} + + )) + )} + + ) : null; +} diff --git a/components/signalement/signalement-diff/signalement-position-diff.tsx b/components/signalement/signalement-diff/signalement-position-diff.tsx new file mode 100644 index 000000000..6bcc7a09e --- /dev/null +++ b/components/signalement/signalement-diff/signalement-position-diff.tsx @@ -0,0 +1,161 @@ +import { Position } from "@/lib/openapi-api-bal"; +import { PositionDTO } from "@/lib/openapi-signalement"; +import { getPositionName } from "@/lib/positions-types-list"; +import { SignalementDiff } from "@/lib/utils/signalement"; +import { + Heading, + MinusIcon, + PlusIcon, + Pane, + Small, + Strong, + Text, + Badge, + ArrowRightIcon, +} from "evergreen-ui"; +import React from "react"; + +interface SignalementPositionDiffProps { + positions: PositionDTO[] | Position[]; + existingPositions?: Position[]; +} + +type PositionDiff = PositionDTO & { + diff: SignalementDiff | [PositionDTO.type, Position.type]; +}; + +export const positionDiff = ( + positions: PositionDTO[], + existingPositions?: Position[] +): PositionDiff[] => { + const deletedPositions = (existingPositions || []) + .filter( + (existingPosition) => + !positions.find( + (position) => + position.point.coordinates.join("-") === + existingPosition.point.coordinates.join("-") + ) + ) + .map((position) => ({ ...position, diff: SignalementDiff.DELETED })); + + const newOrModifiedPositions = positions.map((position) => { + const latLong = position.point.coordinates.join("-"); + + const existingPosition = existingPositions?.find( + (p) => p.point.coordinates.join("-") === latLong + ); + + return { + ...position, + diff: existingPosition + ? [existingPosition.type, position.type] + : SignalementDiff.NEW, + }; + }); + + return [...deletedPositions, ...newOrModifiedPositions] as PositionDiff[]; +}; + +export function SignalementPositionDiff({ + positions, + existingPositions, +}: SignalementPositionDiffProps) { + const showDiff = !!existingPositions; + const isPlural = showDiff + ? positionDiff(positions as PositionDTO[], existingPositions).length > 1 + : positions.length > 1; + + return ( + + Position{isPlural ? "s" : ""} + + + + Lat + + + Long + + + {showDiff + ? positionDiff(positions as PositionDTO[], existingPositions).map( + ({ type, diff, point }, index) => ( + + {Array.isArray(diff) && diff[0] !== diff[1] ? ( + + {getPositionName(diff[0])} + + {getPositionName(diff[1])} + + ) : ( + + {diff === SignalementDiff.DELETED && ( + + )} + {diff === SignalementDiff.NEW && ( + + )} + {getPositionName(type)} + + )} + + {point.coordinates[1].toFixed(6)} + + + {point.coordinates[0].toFixed(6)} + + + ) + ) + : positions.map(({ type, point }, index) => ( + + + {getPositionName(type)} + + + {point.coordinates[1].toFixed(6)} + + + {point.coordinates[0].toFixed(6)} + + + ))} + + + ); +} diff --git a/components/signalement/signalement-diff/signalement-toponyme-diff-card.tsx b/components/signalement/signalement-diff/signalement-toponyme-diff-card.tsx new file mode 100644 index 000000000..635437a35 --- /dev/null +++ b/components/signalement/signalement-diff/signalement-toponyme-diff-card.tsx @@ -0,0 +1,58 @@ +import { Pane } from "evergreen-ui"; +import TextDiff from "./text-diff"; +import { SignalementPositionDiff } from "./signalement-position-diff"; +import { SignalementParcelleDiff } from "./signalement-parcelle-diff"; +import { AccordionCard } from "./accordion-card"; + +interface SignalementToponymeDiffCardProps { + title: string; + nom: { + from?: string; + to: string; + }; + positions: { + from?: any[]; + to: any[]; + }; + parcelles: { + from?: string[]; + to: string[]; + }; + backgroundColor?: string; + isActive?: boolean; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export function SignalementToponymeDiffCard({ + title, + nom, + backgroundColor, + positions, + parcelles, + isActive, + onMouseEnter, + onMouseLeave, +}: SignalementToponymeDiffCardProps) { + return ( + + + + + + + + ); +} diff --git a/components/signalement/signalement-diff/signalement-voie-diff-card.tsx b/components/signalement/signalement-diff/signalement-voie-diff-card.tsx new file mode 100644 index 000000000..6d1d1f037 --- /dev/null +++ b/components/signalement/signalement-diff/signalement-voie-diff-card.tsx @@ -0,0 +1,34 @@ +import { Heading, Pane } from "evergreen-ui"; +import TextDiff from "./text-diff"; + +interface SignalementVoieDiffCardProps { + title: string; + nom: { + from?: string; + to: string; + }; + backgroundColor?: string; +} + +export function SignalementVoieDiffCard({ + title, + nom, + backgroundColor, +}: SignalementVoieDiffCardProps) { + return ( + + + {title} + + + + + + ); +} diff --git a/components/signalement/signalement-diff/text-diff.tsx b/components/signalement/signalement-diff/text-diff.tsx new file mode 100644 index 000000000..2ebb69c4e --- /dev/null +++ b/components/signalement/signalement-diff/text-diff.tsx @@ -0,0 +1,60 @@ +import { Paragraph, Text } from "evergreen-ui"; +import React from "react"; +import fastDiff from "fast-diff"; + +interface TextDiffProps { + from?: string; + to: string; +} + +function TextDiff({ from, to }: TextDiffProps) { + const diffStr = (from && fastDiff(from, to)) || []; + + return ( + + {diffStr.length > 1 ? ( + <> + + {diffStr.map((diff, index) => { + const [operation, text] = diff; + switch (operation) { + case fastDiff.DELETE: + return ( + + {text} + + ); + case fastDiff.INSERT: + return ( + + {text} + + ); + default: + return {text}; + } + })} + + + ) : ( + {to} + )} + + ); +} + +export default TextDiff; diff --git a/components/signalement/signalement-form/numero/signalement-create-numero.tsx b/components/signalement/signalement-form/numero/signalement-create-numero.tsx new file mode 100644 index 000000000..8266ea77b --- /dev/null +++ b/components/signalement/signalement-form/numero/signalement-create-numero.tsx @@ -0,0 +1,134 @@ +import React, { useContext, useEffect } from "react"; +import { Toponyme, Voie, VoiesService } from "@/lib/openapi-api-bal"; +import { + NumeroChangesRequestedDTO, + Signalement, + Position as PositionSignalement, +} from "@/lib/openapi-signalement"; +import { SignalementFormButtons } from "../signalement-form-buttons"; +import MarkersContext from "@/contexts/markers"; +import { getPositionName } from "@/lib/positions-types-list"; +import MapContext from "@/contexts/map"; +import ParcellesContext from "@/contexts/parcelles"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; + +interface SignalementCreateNumeroProps { + signalement: Signalement; + voie: Voie; + requestedToponyme?: Toponyme; + handleAccept: () => Promise; + handleReject: () => Promise; + handleClose: () => void; + isLoading: boolean; +} + +function SignalementCreateNumero({ + signalement, + voie, + requestedToponyme, + handleAccept, + handleReject, + handleClose, + isLoading, +}: SignalementCreateNumeroProps) { + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + const { numero, suffixe, parcelles, positions, nomVoie } = + signalement.changesRequested as NumeroChangesRequestedDTO; + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (positions?.length > 0) { + positions.forEach((position: PositionSignalement & { id: string }) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: "gray", + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + label: getPositionName(position.type), + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positions, addMarker, disableMarkers]); + + const onAccept = async () => { + await VoiesService.createNumero(voie.id, { + numero, + suffixe, + positions: positions as any[], + parcelles, + certifie: true, + toponymeId: requestedToponyme?.id, + }); + await handleAccept(); + }; + + return ( + <> + + + + + ); +} + +export default SignalementCreateNumero; diff --git a/components/signalement/signalement-form/numero/signalement-delete-numero.tsx b/components/signalement/signalement-form/numero/signalement-delete-numero.tsx new file mode 100644 index 000000000..52b7a6557 --- /dev/null +++ b/components/signalement/signalement-form/numero/signalement-delete-numero.tsx @@ -0,0 +1,117 @@ +import React, { useContext, useEffect } from "react"; +import MapContext from "@/contexts/map"; +import { Numero, NumerosService } from "@/lib/openapi-api-bal"; +import { SignalementFormButtons } from "../signalement-form-buttons"; +import { getPositionName } from "@/lib/positions-types-list"; +import MarkersContext from "@/contexts/markers"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import ParcellesContext from "@/contexts/parcelles"; +import { signalementTypeMap } from "../../signalement-type-badge"; +import { Signalement } from "@/lib/openapi-signalement"; + +interface SignalementDeleteNumeroProps { + existingLocation: Numero; + handleAccept: () => Promise; + handleReject: () => Promise; + handleClose: () => void; + isLoading: boolean; +} + +function SignalementDeleteNumero({ + existingLocation, + handleAccept, + handleReject, + handleClose, + isLoading, +}: SignalementDeleteNumeroProps) { + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + const { numero, suffixe, voie, parcelles, positions } = existingLocation; + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (positions.length > 0) { + positions.forEach((position) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: "gray", + label: getPositionName(position.type), + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positions, addMarker, disableMarkers]); + + const onAccept = async () => { + await NumerosService.softDeleteNumero(existingLocation.id); + await handleAccept(); + }; + + return ( + <> + + + + ); +} + +export default SignalementDeleteNumero; diff --git a/components/signalement/signalement-form/numero/signalement-update-numero.tsx b/components/signalement/signalement-form/numero/signalement-update-numero.tsx new file mode 100644 index 000000000..00e3e8e28 --- /dev/null +++ b/components/signalement/signalement-form/numero/signalement-update-numero.tsx @@ -0,0 +1,162 @@ +import React, { useState } from "react"; +import { + Numero, + NumerosService, + Toponyme, + VoiesService, +} from "@/lib/openapi-api-bal"; +import { + Signalement, + NumeroChangesRequestedDTO, +} from "@/lib/openapi-signalement"; +import { SignalementFormButtons } from "../signalement-form-buttons"; +import { ActiveCardEnum, detectChanges } from "@/lib/utils/signalement"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; +import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; + +interface SignalementUpdateNumeroProps { + signalement: Signalement; + existingLocation: Numero; + requestedToponyme?: Toponyme; + handleAccept: () => Promise; + handleReject: () => Promise; + handleClose: () => void; + isLoading: boolean; +} + +function SignalementUpdateNumero({ + signalement, + existingLocation, + requestedToponyme, + handleAccept, + handleReject, + handleClose, + isLoading, +}: SignalementUpdateNumeroProps) { + const [changes] = useState(detectChanges(signalement, existingLocation)); + + const { numero, suffixe, positions, parcelles, nomVoie } = + signalement.changesRequested as NumeroChangesRequestedDTO; + + const { + numero: existingNumero, + suffixe: existingSuffixe, + positions: existingPositions, + parcelles: existingParcelles, + voie: existingVoie, + } = existingLocation; + + const { activeCard, setActiveCard } = useSignalementMapDiff( + { positions: existingPositions, parcelles: existingParcelles }, + { positions, parcelles } + ); + + const onAccept = async () => { + if (changes.voie) { + await VoiesService.updateVoie(existingLocation.voie.id, { + nom: nomVoie, + }); + } + await NumerosService.updateNumero(existingLocation.id, { + numero, + suffixe, + positions: positions as any[], + parcelles, + toponymeId: requestedToponyme?.id, + }); + await handleAccept(); + }; + + return ( + <> + { + setActiveCard(ActiveCardEnum.INITIAL); + }} + /> + { + setActiveCard(ActiveCardEnum.CHANGES); + }} + /> + { + setActiveCard(ActiveCardEnum.FINAL); + }} + /> + + + ); +} + +export default SignalementUpdateNumero; diff --git a/components/signalement/signalement-form/signalement-form-buttons.tsx b/components/signalement/signalement-form/signalement-form-buttons.tsx new file mode 100644 index 000000000..55051903d --- /dev/null +++ b/components/signalement/signalement-form/signalement-form-buttons.tsx @@ -0,0 +1,159 @@ +import { DelayBar } from "@/components/delay-bar"; +import { BanCircleIcon, Button, EndorsedIcon, Pane } from "evergreen-ui"; +import { useRef, useState } from "react"; + +interface SignalementFormButtonsProps { + isLoading: boolean; + onAccept: () => Promise; + onReject: () => Promise; + onClose: () => void; +} + +const CONFIRMATION_DELAY = 5000; + +export function SignalementFormButtons({ + isLoading, + onAccept, + onReject, + onClose, +}: SignalementFormButtonsProps) { + const [actionToConfirm, setActionToConfirm] = useState< + "accept" | "reject" | null + >(null); + const timeOutRef = useRef(null); + + const handleActionToConfirm = (action: "accept" | "reject") => { + setActionToConfirm(action); + timeOutRef.current = setTimeout(() => { + setActionToConfirm(null); + }, CONFIRMATION_DELAY); + }; + + const handleConfirm = async () => { + if (actionToConfirm) { + if (actionToConfirm === "accept") { + await onAccept(); + } else if (actionToConfirm === "reject") { + await onReject(); + } + handleClear(); + } + }; + + const handleClear = () => { + clearTimeout(timeOutRef.current); + setActionToConfirm(null); + }; + + return ( + + {actionToConfirm ? ( + + + + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + + )} + + ); +} diff --git a/components/signalement/signalement-form/signalement-form.tsx b/components/signalement/signalement-form/signalement-form.tsx new file mode 100644 index 000000000..0e3509305 --- /dev/null +++ b/components/signalement/signalement-form/signalement-form.tsx @@ -0,0 +1,159 @@ +import React, { useContext, useEffect, useState } from "react"; +import { + ExistingLocation, + NumeroChangesRequestedDTO, + Signalement, +} from "@/lib/openapi-signalement"; +import { Numero, Toponyme, Voie } from "@/lib/openapi-api-bal"; +import Form from "../../form"; +import SignalementCreateNumero from "./numero/signalement-create-numero"; +import SignalementUpdateNumero from "./numero/signalement-update-numero"; +import SignalementUpdateVoie from "./voie/signalement-update-voie"; +import SignalementUpdateToponyme from "./toponyme/signalement-update-toponyme"; +import SignalementDeleteNumero from "./numero/signalement-delete-numero"; +import MapContext from "@/contexts/map"; +import { SignalementHeader } from "../signalement-header"; + +interface SignalementFormProps { + signalement: Signalement; + existingLocation: Voie | Toponyme | Numero; + requestedToponyme?: Toponyme; + onSubmit: (status: Signalement.status) => Promise; + onClose: () => void; +} + +function SignalementForm({ + signalement, + existingLocation, + requestedToponyme, + onSubmit, + onClose, +}: SignalementFormProps) { + const [isLoading, setIsLoading] = useState(false); + const { setViewport } = useContext(MapContext); + + // Point the map to the location of the signalement + useEffect(() => { + let pointTo = null; + + if ((existingLocation as Numero).positions?.length > 0) { + const position = (existingLocation as Numero).positions[0]; + pointTo = { + latitude: position.point.coordinates[1], + longitude: position.point.coordinates[0], + }; + } else if ((existingLocation as Voie).centroid) { + pointTo = { + latitude: (existingLocation as Voie).centroid.coordinates[1], + longitude: (existingLocation as Voie).centroid.coordinates[0], + }; + } else if ( + (signalement.changesRequested as NumeroChangesRequestedDTO).positions + ?.length > 0 + ) { + const position = ( + signalement.changesRequested as NumeroChangesRequestedDTO + ).positions[0]; + pointTo = { + latitude: position.point.coordinates[1], + longitude: position.point.coordinates[0], + }; + } + + if (pointTo) { + setViewport({ + latitude: pointTo.latitude, + longitude: pointTo.longitude, + zoom: 20, + }); + } + }, [existingLocation, signalement.changesRequested, setViewport]); + + const handleSubmit = async (status: Signalement.status) => { + try { + setIsLoading(true); + await onSubmit(status); + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } + }; + + const handleAccept = async () => { + await handleSubmit(Signalement.status.PROCESSED); + }; + + const handleReject = async () => { + await handleSubmit(Signalement.status.IGNORED); + }; + + return ( +
{ + e.preventDefault(); + + return Promise.resolve(); + }} + > + + + {signalement.type === Signalement.type.LOCATION_TO_CREATE && ( + + )} + + {signalement.type === Signalement.type.LOCATION_TO_UPDATE && + (signalement.existingLocation.type === ExistingLocation.type.NUMERO ? ( + + ) : signalement.existingLocation.type === ExistingLocation.type.VOIE ? ( + + ) : ( + + ))} + + {signalement.type === Signalement.type.LOCATION_TO_DELETE && ( + + )} + + ); +} + +export default SignalementForm; diff --git a/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx b/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx new file mode 100644 index 000000000..ccd64a90a --- /dev/null +++ b/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { + Signalement, + ToponymeChangesRequestedDTO, +} from "@/lib/openapi-signalement"; +import { Toponyme, ToponymesService } from "@/lib/openapi-api-bal"; +import { SignalementFormButtons } from "../signalement-form-buttons"; +import { SignalementToponymeDiffCard } from "../../signalement-diff/signalement-toponyme-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; +import { ActiveCardEnum } from "@/lib/utils/signalement"; +import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; + +interface SignalementUpdateToponymeProps { + signalement: Signalement; + existingLocation: Toponyme; + handleAccept: () => Promise; + handleReject: () => Promise; + handleClose: () => void; + isLoading: boolean; +} + +function SignalementUpdateToponyme({ + signalement, + existingLocation, + handleAccept, + handleReject, + handleClose, + isLoading, +}: SignalementUpdateToponymeProps) { + const { + nom: existingNom, + positions: existingPositions, + parcelles: existingParcelles, + } = existingLocation; + + const { nom, parcelles, positions } = + signalement.changesRequested as ToponymeChangesRequestedDTO; + + const { activeCard, setActiveCard } = useSignalementMapDiff( + { positions: existingPositions, parcelles: existingParcelles }, + { positions, parcelles } + ); + + const onAccept = async () => { + await ToponymesService.updateToponyme(existingLocation.id, { + nom, + }); + await handleAccept(); + }; + + return ( + <> + { + setActiveCard(ActiveCardEnum.INITIAL); + }} + /> + { + setActiveCard(ActiveCardEnum.CHANGES); + }} + /> + { + setActiveCard(ActiveCardEnum.FINAL); + }} + /> + + + ); +} + +export default SignalementUpdateToponyme; diff --git a/components/signalement/signalement-form/voie/signalement-update-voie.tsx b/components/signalement/signalement-form/voie/signalement-update-voie.tsx new file mode 100644 index 000000000..7d5567de9 --- /dev/null +++ b/components/signalement/signalement-form/voie/signalement-update-voie.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { + Signalement, + VoieChangesRequestedDTO, +} from "@/lib/openapi-signalement"; +import { Voie, VoiesService } from "@/lib/openapi-api-bal"; +import { SignalementFormButtons } from "../signalement-form-buttons"; +import { SignalementVoieDiffCard } from "../../signalement-diff/signalement-voie-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; + +interface SignalementUpdateVoieProps { + signalement: Signalement; + existingLocation: Voie; + handleAccept: () => Promise; + handleReject: () => Promise; + handleClose: () => void; + isLoading: boolean; +} + +function SignalementUpdateVoie({ + signalement, + existingLocation, + handleAccept, + handleReject, + handleClose, + isLoading, +}: SignalementUpdateVoieProps) { + const { nom: existingNom } = existingLocation; + const { nom } = signalement.changesRequested as VoieChangesRequestedDTO; + + const onAccept = async () => { + await VoiesService.updateVoie(existingLocation.id, { + nom, + }); + await handleAccept(); + }; + + return ( + <> + + + + + + ); +} + +export default SignalementUpdateVoie; diff --git a/components/signalement/signalement-header.tsx b/components/signalement/signalement-header.tsx new file mode 100644 index 000000000..3623c5365 --- /dev/null +++ b/components/signalement/signalement-header.tsx @@ -0,0 +1,72 @@ +import { Alert, Pane, Paragraph } from "evergreen-ui"; +import SignalementTypeBadge from "./signalement-type-badge"; +import { Signalement } from "@/lib/openapi-signalement"; +import { getDuration } from "@/lib/utils/date"; + +interface SignalementHeaderProps { + signalement: Signalement; +} + +const MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30; + +export function SignalementHeader({ signalement }: SignalementHeaderProps) { + return ( + } + intent="info" + padding={8} + borderRadius={8} + marginBottom={8} + width="100%" + flexShrink={0} + > + + {Date.now() - new Date(signalement.createdAt).getTime() > + MONTH_IN_MS ? ( + + Déposée le{" "} + {new Date(signalement.createdAt).toLocaleDateString()}{" "} + + ) : ( + + Déposée il y a {getDuration(new Date(signalement.createdAt))}{" "} + + )} + + via {signalement.source.nom} + + + {signalement.changesRequested.comment && ( + + Commentaire : + {signalement.changesRequested.comment} + + )} + + {signalement.status === Signalement.status.PROCESSED && ( + <> + + Acceptée le{" "} + {new Date(signalement.updatedAt).toLocaleDateString()} + + + via {signalement.processedBy.nom} + + + )} + + {signalement.status === Signalement.status.IGNORED && ( + <> + + Refusée le{" "} + {new Date(signalement.updatedAt).toLocaleDateString()} + + + via {signalement.processedBy.nom} + + + )} + + + ); +} diff --git a/components/signalement/signalement-list-item.tsx b/components/signalement/signalement-list-item.tsx new file mode 100644 index 000000000..90d9ee536 --- /dev/null +++ b/components/signalement/signalement-list-item.tsx @@ -0,0 +1,116 @@ +import { Signalement } from "@/lib/openapi-signalement"; +import { + Table, + Popover, + Menu, + Position, + IconButton, + MoreIcon, + SendToMapIcon, + TrashIcon, + Checkbox, + Icon, + EndorsedIcon, + BanCircleIcon, + Tooltip, +} from "evergreen-ui"; +import SignalementTypeBadge from "./signalement-type-badge"; + +interface SignalementListItemProps { + signalement: Signalement & { label: string }; + editionEnabled?: boolean; + onToggleSelect: (ids: string[]) => void; + selectedSignalements: string[]; + onHoverEnter: (id: string) => void; + onHoverLeave: (id: string) => void; + onSelect: (id: string) => void; + onIgnore: (id: string) => void; +} + +export function SignalementListItem({ + signalement, + editionEnabled, + selectedSignalements, + onToggleSelect, + onHoverEnter, + onHoverLeave, + onSelect, + onIgnore, +}: SignalementListItemProps) { + return ( + + + {editionEnabled ? ( + onToggleSelect([signalement.id])} + /> + ) : signalement.status === Signalement.status.IGNORED ? ( + + + + ) : signalement.status === Signalement.status.PROCESSED ? ( + + + + ) : null} + + onHoverEnter(signalement.id)} + onMouseLeave={() => onHoverLeave(signalement.id)} + onClick={() => onSelect(signalement.id)} + cursor="pointer" + > + {signalement.label} + + {editionEnabled && ( + + + + onSelect(signalement.id)} + > + Traiter + + onIgnore(signalement.id)} + > + Ignorer + + + + } + > + + + + )} + + ); +} diff --git a/components/signalement/signalement-list.tsx b/components/signalement/signalement-list.tsx index 431c93b96..bc4ed7b41 100644 --- a/components/signalement/signalement-list.tsx +++ b/components/signalement/signalement-list.tsx @@ -1,129 +1,159 @@ -import { useMemo } from "react"; -import { sortBy } from "lodash"; +import { useContext, useMemo, useState } from "react"; import { Table, - Popover, - Menu, - Position, - IconButton, - MoreIcon, - SendToMapIcon, - TrashIcon, Checkbox, + Button, + FilterRemoveIcon, + FilterIcon, + Tooltip, + Pane, } from "evergreen-ui"; -import { normalizeSort } from "@/lib/normalize"; - -import useFuse from "@/hooks/fuse"; - import InfiniteScrollList from "../infinite-scroll-list"; -import { getSignalementLabel } from "@/lib/utils/signalement"; +import { Signalement } from "@/lib/openapi-signalement"; +import SignalementTypeBadge from "./signalement-type-badge"; +import MarkersContext from "@/contexts/markers"; +import { SignalementListItem } from "./signalement-list-item"; interface SignalementListProps { - signalements: any[]; + signalements: Signalement[]; selectedSignalements: string[]; + setSelectedSignalements: (ids: string[]) => void; onSelect: (id: string) => void; onIgnore: (id: string) => void; onToggleSelect: (ids: string[]) => void; + filters: { + type: Signalement.type[]; + }; + setFilters: (filters: { type: Signalement.type[] }) => void; + onSearch?: (search: string) => void; + editionEnabled?: boolean; } function SignalementList({ signalements, selectedSignalements, + setSelectedSignalements, onSelect, onIgnore, onToggleSelect, + filters, + setFilters, + onSearch, + editionEnabled, }: SignalementListProps) { - const signalementWithLabel = useMemo( - () => signalements.map((s) => ({ ...s, label: getSignalementLabel(s) })), - [signalements] - ); + const [showFilters, setShowFilters] = useState(false); + const { updateMarker } = useContext(MarkersContext); - const [filtered, setFilter] = useFuse(signalementWithLabel, 200, { - keys: ["label"], - }); - - const scrollableItems = useMemo( - () => sortBy(filtered, (s) => normalizeSort(s.label)), - [filtered] + const hasActiveFilters = useMemo( + () => + Object.keys(filters).reduce((acc, cur) => { + return acc || filters[cur].length > 0; + }, false), + [filters] ); const isAllSelected = useMemo(() => { const isAllSignalementsSelected = - filtered.length === selectedSignalements.length && filtered.length > 0; + signalements.length === selectedSignalements.length && + signalements.length > 0; return isAllSignalementsSelected; - }, [selectedSignalements, filtered]); + }, [selectedSignalements, signalements]); + + const onHoverEnter = (id: string) => { + updateMarker(id, { showTooltip: true }); + }; + + const onHoverLeave = (id: string) => { + updateMarker(id, { showTooltip: false }); + }; return ( - - - onToggleSelect( - isAllSelected ? [] : signalements.map(({ _id }) => _id) - ) - } - /> - + {editionEnabled && ( + + + setSelectedSignalements( + isAllSelected ? [] : signalements.map(({ id }) => id) + ) + } + /> + + )} + + + {Object.values(Signalement.type).map((type) => ( + } + checked={filters.type.includes(type)} + onChange={() => + setFilters({ + type: filters.type.includes(type) + ? filters.type.filter((t) => t !== type) + : [...filters.type, type], + }) + } + /> + ))} + + } + appearance="card" + isShown={showFilters} + > + + + {showFilters && ( + setShowFilters(false)} + /> + )} + - {filtered.length === 0 && ( + {signalements.length === 0 && ( - - Aucun résultat + + Vous n'avez aucune proposition actuellement )} - - {(signalement) => ( - - - onToggleSelect([signalement._id])} - /> - - {signalement.label} - - - - onSelect(signalement._id)} - > - Traiter - - onIgnore(signalement._id)} - > - Ignorer - - - - } - > - - - - + + {(signalement: Signalement & { label: string }) => ( + )}
diff --git a/components/signalement/signalement-type-badge.tsx b/components/signalement/signalement-type-badge.tsx new file mode 100644 index 000000000..089d9f7c4 --- /dev/null +++ b/components/signalement/signalement-type-badge.tsx @@ -0,0 +1,41 @@ +import { Signalement } from "@/lib/openapi-signalement"; +import { Badge } from "evergreen-ui"; + +interface SignalementTypeBadgeProps { + type: Signalement.type; +} + +export const signalementTypeMap = { + [Signalement.type.LOCATION_TO_CREATE]: { + label: "Création", + color: "teal", + backgroundColor: "#D3F5F7", + foregroundColor: "#0F5156", + }, + [Signalement.type.LOCATION_TO_UPDATE]: { + label: "Modification", + color: "purple", + backgroundColor: "#E7E4F9", + foregroundColor: "#6E62B6", + }, + [Signalement.type.LOCATION_TO_DELETE]: { + label: "Suppression", + color: "orange", + backgroundColor: "#F8E3DA", + foregroundColor: "#FFB020", + }, +}; + +function SignalementTypeBadge({ type }: SignalementTypeBadgeProps) { + return ( + + {signalementTypeMap[type].label} + + ); +} + +export default SignalementTypeBadge; diff --git a/components/signalement/signalement-viewer.tsx b/components/signalement/signalement-viewer.tsx deleted file mode 100644 index 8a0595cd7..000000000 --- a/components/signalement/signalement-viewer.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Pane, Paragraph } from "evergreen-ui"; -import React from "react"; -import { positionsTypesList } from "@/lib/positions-types-list"; -import { Signalement } from "@/lib/openapi-signalement"; - -interface SignalementViewerProps { - signalement: any; - existingLocation?: any; -} - -const getPositionTypeLabel = (positionType: string) => { - return positionsTypesList.find((p) => p.value === positionType)?.name; -}; - -function SignalementViewer({ - signalement, - existingLocation, -}: SignalementViewerProps) { - const { numero, suffixe, nomVoie, positions, parcelles, nom } = - signalement.changesRequested; - - return ( - - {signalement.type !== Signalement.type.LOCATION_TO_CREATE && ( - -

Lieu existant

-
- {existingLocation.numero} {existingLocation.suffixe}{" "} - {existingLocation.nomVoie} {existingLocation.nom} -
- {existingLocation.positions && ( - <> -

Positions :

- {existingLocation.positions.map(({ point, type }, index) => { - return ( - - {getPositionTypeLabel(type)} :{" "} - {point.coordinates[0]}, {point.coordinates[1]} -
-
- ); // eslint-disable-line react/no-array-index-key - })} - - )} - {existingLocation.parcelles && ( - <> -

Parcelles :

-
- {existingLocation.parcelles.map((parcelle) => ( -
{parcelle}
- ))} -
- - )} -
- )} - {(signalement.type === Signalement.type.LOCATION_TO_UPDATE || - signalement.type === Signalement.type.LOCATION_TO_CREATE) && ( - - {signalement.type === Signalement.type.LOCATION_TO_UPDATE && ( -

Modifications demandées

- )} - {signalement.type === Signalement.type.LOCATION_TO_CREATE && ( -

Création demandée

- )} -
- {numero} {suffixe} {nomVoie} {nom} -
- {positions && ( - <> -

Positions :

- {positions.map(({ point, type }, index) => { - return ( - - {getPositionTypeLabel(type)} :{" "} - {point.coordinates[0]}, {point.coordinates[1]} -
-
- ); // eslint-disable-line react/no-array-index-key - })} - - )} - {parcelles && ( - <> -

Parcelles :

-
- {parcelles.map((parcelle) => ( -
{parcelle}
- ))} -
- - )} -
- )} - - {signalement.type === Signalement.type.LOCATION_TO_DELETE && ( - -

Commentaire :

-
{signalement.comment}
-
- )} - - -

Auteur du signalement

- - -

Nom

-
{signalement.author?.lastName}
-
- -

Prénom

-
{signalement.author?.firstName}
-
- -

Email

-
{signalement.author?.email}
-
-
-
- - -
- ); -} - -export default SignalementViewer; diff --git a/components/signalement/signalement-viewer/numero/signalement-viewer-create-numero.tsx b/components/signalement/signalement-viewer/numero/signalement-viewer-create-numero.tsx new file mode 100644 index 000000000..6584a5cb3 --- /dev/null +++ b/components/signalement/signalement-viewer/numero/signalement-viewer-create-numero.tsx @@ -0,0 +1,104 @@ +import React, { useContext, useEffect } from "react"; +import { + NumeroChangesRequestedDTO, + Signalement, + Position as PositionSignalement, +} from "@/lib/openapi-signalement"; +import MarkersContext from "@/contexts/markers"; +import { getPositionName } from "@/lib/positions-types-list"; +import MapContext from "@/contexts/map"; +import ParcellesContext from "@/contexts/parcelles"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; + +interface SignalementViewerCreateNumeroProps { + signalement: Signalement; +} + +function SignalementViewerCreateNumero({ + signalement, +}: SignalementViewerCreateNumeroProps) { + const { changesRequested, status } = signalement; + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + const { numero, suffixe, parcelles, positions, nomVoie, nomComplement } = + changesRequested as NumeroChangesRequestedDTO; + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (positions?.length > 0) { + positions.forEach((position: PositionSignalement & { id: string }) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: "grey", + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + label: getPositionName(position.type), + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positions, addMarker, disableMarkers]); + + return ( + <> + + + ); +} + +export default SignalementViewerCreateNumero; diff --git a/components/signalement/signalement-viewer/numero/signalement-viewer-delete-numero.tsx b/components/signalement/signalement-viewer/numero/signalement-viewer-delete-numero.tsx new file mode 100644 index 000000000..f9ef181da --- /dev/null +++ b/components/signalement/signalement-viewer/numero/signalement-viewer-delete-numero.tsx @@ -0,0 +1,96 @@ +import React, { useContext, useEffect } from "react"; +import MapContext from "@/contexts/map"; +import { getPositionName } from "@/lib/positions-types-list"; +import MarkersContext from "@/contexts/markers"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import ParcellesContext from "@/contexts/parcelles"; +import { ExistingNumero, Signalement } from "@/lib/openapi-signalement"; +import { signalementTypeMap } from "../../signalement-type-badge"; + +interface SignalementViewerDeleteNumeroProps { + signalement: Signalement; +} + +function SignalementViewerDeleteNumero({ + signalement, +}: SignalementViewerDeleteNumeroProps) { + const { existingLocation, status } = signalement; + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + const { numero, suffixe, toponyme, parcelles, position, nomComplement } = + existingLocation as ExistingNumero; + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + addMarker({ + id: "deleted-numero", + isMapMarker: true, + isDisabled: true, + color: "grey", + label: getPositionName(position.type), + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + }); + + return () => { + disableMarkers(); + }; + }, [position, addMarker, disableMarkers]); + + return ( + <> + + + ); +} + +export default SignalementViewerDeleteNumero; diff --git a/components/signalement/signalement-viewer/numero/signalement-viewer-update-numero.tsx b/components/signalement/signalement-viewer/numero/signalement-viewer-update-numero.tsx new file mode 100644 index 000000000..a2b904407 --- /dev/null +++ b/components/signalement/signalement-viewer/numero/signalement-viewer-update-numero.tsx @@ -0,0 +1,130 @@ +import React, { useMemo } from "react"; +import { + Signalement, + NumeroChangesRequestedDTO, + ExistingNumero, +} from "@/lib/openapi-signalement"; +import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; +import { ActiveCardEnum } from "@/lib/utils/signalement"; +import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; + +interface SignalementViewerUpdateNumeroProps { + signalement: Signalement; +} + +function SignalementViewerUpdateNumero({ + signalement, +}: SignalementViewerUpdateNumeroProps) { + const { changesRequested, existingLocation, status } = signalement; + + const { numero, suffixe, positions, parcelles, nomVoie, nomComplement } = + changesRequested as NumeroChangesRequestedDTO; + + const { + numero: existingNumero, + suffixe: existingSuffixe, + position: existingPosition, + parcelles: existingParcelles, + toponyme: existingVoie, + nomComplement: existingNomComplement, + } = existingLocation as ExistingNumero; + + const existingPositions = useMemo( + () => [existingPosition], + [existingPosition] + ); + + const { activeCard, setActiveCard } = useSignalementMapDiff( + { positions: existingPositions, parcelles: existingParcelles }, + { positions, parcelles } + ); + + return ( + <> + { + setActiveCard(ActiveCardEnum.INITIAL); + }} + /> + { + setActiveCard(ActiveCardEnum.CHANGES); + }} + /> + { + setActiveCard(ActiveCardEnum.FINAL); + }} + /> + + ); +} + +export default SignalementViewerUpdateNumero; diff --git a/components/signalement/signalement-viewer/signalement-viewer.tsx b/components/signalement/signalement-viewer/signalement-viewer.tsx new file mode 100644 index 000000000..9eea7f872 --- /dev/null +++ b/components/signalement/signalement-viewer/signalement-viewer.tsx @@ -0,0 +1,112 @@ +import { + ExistingLocation, + ExistingNumero, + NumeroChangesRequestedDTO, + Signalement, +} from "@/lib/openapi-signalement"; +import { SignalementHeader } from "../signalement-header"; +import Form from "@/components/form"; +import { Button, Pane } from "evergreen-ui"; +import { useContext, useEffect } from "react"; +import MapContext from "@/contexts/map"; +import SignalementViewerUpdateNumero from "./numero/signalement-viewer-update-numero"; +import SignalementViewerCreateNumero from "./numero/signalement-viewer-create-numero"; +import SignalementViewerUpdateVoie from "./voie/signalement-viewer-update-voie"; +import SignalementViewerUpdateToponyme from "./toponyme/signalement-viewer-update-toponyme"; +import SignalementViewerDeleteNumero from "./numero/signalement-viewer-delete-numero"; + +interface SignalementViewerProps { + signalement: Signalement; + onClose: () => void; +} + +export function SignalementViewer({ + signalement, + onClose, +}: SignalementViewerProps) { + const { setViewport } = useContext(MapContext); + + // Point the map to the location of the signalement + useEffect(() => { + let pointTo = null; + const { changesRequested, existingLocation } = signalement; + + if ((changesRequested as NumeroChangesRequestedDTO).positions?.length > 0) { + const position = (changesRequested as NumeroChangesRequestedDTO) + .positions[0]; + pointTo = { + latitude: position.point.coordinates[1], + longitude: position.point.coordinates[0], + }; + } else if ((existingLocation as ExistingNumero)?.position) { + const position = (existingLocation as ExistingNumero).position; + pointTo = { + latitude: position.point.coordinates[1], + longitude: position.point.coordinates[0], + }; + } + + pointTo && + setViewport({ + latitude: pointTo.latitude, + longitude: pointTo.longitude, + zoom: 20, + }); + }, [signalement, setViewport]); + + return ( +
{ + e.preventDefault(); + + return Promise.resolve(); + }} + > + + + {signalement.type === Signalement.type.LOCATION_TO_CREATE && ( + + )} + + {signalement.type === Signalement.type.LOCATION_TO_UPDATE && + (signalement.existingLocation.type === ExistingLocation.type.NUMERO ? ( + + ) : signalement.existingLocation.type === ExistingLocation.type.VOIE ? ( + + ) : ( + + ))} + + {signalement.type === Signalement.type.LOCATION_TO_DELETE && ( + + )} + + + + + + + + ); +} diff --git a/components/signalement/signalement-viewer/toponyme/signalement-viewer-update-toponyme.tsx b/components/signalement/signalement-viewer/toponyme/signalement-viewer-update-toponyme.tsx new file mode 100644 index 000000000..db78c8318 --- /dev/null +++ b/components/signalement/signalement-viewer/toponyme/signalement-viewer-update-toponyme.tsx @@ -0,0 +1,103 @@ +import React, { useMemo } from "react"; +import { + ExistingToponyme, + Signalement, + ToponymeChangesRequestedDTO, +} from "@/lib/openapi-signalement"; +import { SignalementToponymeDiffCard } from "../../signalement-diff/signalement-toponyme-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; +import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; +import { ActiveCardEnum } from "@/lib/utils/signalement"; + +interface SignalementViewerUpdateToponymeProps { + signalement: Signalement; +} + +function SignalementViewerUpdateToponyme({ + signalement, +}: SignalementViewerUpdateToponymeProps) { + const { existingLocation, changesRequested, status } = signalement; + + const { + nom: existingNom, + position: existingPosition, + parcelles: existingParcelles, + } = existingLocation as ExistingToponyme; + + const { nom, positions, parcelles } = + changesRequested as ToponymeChangesRequestedDTO; + + const existingPositions = useMemo( + () => [existingPosition], + [existingPosition] + ); + + const { activeCard, setActiveCard } = useSignalementMapDiff( + { positions: existingPositions, parcelles: existingParcelles }, + { positions, parcelles } + ); + + return ( + <> + { + setActiveCard(ActiveCardEnum.INITIAL); + }} + /> + { + setActiveCard(ActiveCardEnum.CHANGES); + }} + /> + { + setActiveCard(ActiveCardEnum.FINAL); + }} + /> + + ); +} + +export default SignalementViewerUpdateToponyme; diff --git a/components/signalement/signalement-viewer/voie/signalement-viewer-update-voie.tsx b/components/signalement/signalement-viewer/voie/signalement-viewer-update-voie.tsx new file mode 100644 index 000000000..8ee03fb97 --- /dev/null +++ b/components/signalement/signalement-viewer/voie/signalement-viewer-update-voie.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { + ExistingVoie, + Signalement, + VoieChangesRequestedDTO, +} from "@/lib/openapi-signalement"; +import { SignalementVoieDiffCard } from "../../signalement-diff/signalement-voie-diff-card"; +import { signalementTypeMap } from "../../signalement-type-badge"; + +interface SignalementViewerUpdateVoieProps { + signalement: Signalement; +} + +function SignalementViewerUpdateVoie({ + signalement, +}: SignalementViewerUpdateVoieProps) { + const { existingLocation, changesRequested, status } = signalement; + const { nom: existingNom } = existingLocation as ExistingVoie; + const { nom } = changesRequested as VoieChangesRequestedDTO; + + return ( + <> + + + + + ); +} + +export default SignalementViewerUpdateVoie; diff --git a/components/signalement/toponyme/signalement-update-toponyme.tsx b/components/signalement/toponyme/signalement-update-toponyme.tsx deleted file mode 100644 index 92a639d4d..000000000 --- a/components/signalement/toponyme/signalement-update-toponyme.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { Badge, Pane, Paragraph, Strong } from "evergreen-ui"; -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; -import ReactDOM from "react-dom"; -import { CommuneType } from "@/types/commune"; -import SignalementCard from "../signalement-card"; -import ToponymeEditor from "@/components/bal/toponyme-editor"; -import { Position, Toponyme } from "@/lib/openapi-api-bal"; -import PositionItem from "@/components/bal/position-item"; -import MarkersContext from "@/contexts/markers"; -import { - Signalement, - Position as PositionSignalement, -} from "@/lib/openapi-signalement"; - -interface SignalementUpdateToponymeProps { - signalement: Signalement; - existingLocation: Toponyme; - handleSubmit: () => Promise; - handleClose: () => void; - commune: CommuneType; -} - -const detectChanges = (signalement, existingLocation: Toponyme) => { - const { nom } = signalement.changesRequested; - - const { nom: existingNom } = existingLocation; - - return { - nom: existingNom !== nom, - positions: false, - parcelles: false, - // positions: - // JSON.stringify(positions.map(({ point, type }) => ({ point, type }))) !== - // JSON.stringify( - // existingPositions.map(({ point, type }) => ({ point, type })) - // ), - // parcelles: JSON.stringify(parcelles) !== JSON.stringify(existingParcelles), - }; -}; - -function SignalementUpdateToponyme({ - signalement, - existingLocation, - handleSubmit, - handleClose, - commune, -}: SignalementUpdateToponymeProps) { - const { nom, positions, parcelles } = signalement.changesRequested; - - const nomInputRef = useRef(null); - const positionsInputRef = useRef(null); - const parcellesInputRef = useRef(null); - - const refs = useMemo( - () => ({ - nom: nomInputRef, - positions: positionsInputRef, - parcelles: parcellesInputRef, - }), - [] - ); - - const { markers, addMarker, removeMarker, disableMarkers } = - useContext(MarkersContext); - - const [changes, setChanges] = useState( - detectChanges(signalement, existingLocation) - ); - const [refsInitialized, setRefsInitialized] = useState(false); - const [editorValue, setEditorValue] = useState(existingLocation); - - useEffect(() => { - const refKeys = Object.keys(refs); - refKeys.forEach((key) => { - if (refs[key].current) { - changes[key] - ? (refs[key].current.style.border = "solid #f3b346 2px") - : (refs[key].current.style.border = "none"); - } - }); - if (refKeys.every((key) => Boolean(refs[key].current))) { - setRefsInitialized(true); - } - }, [refs, changes]); - - useEffect(() => { - if (positions) { - positions.forEach((position: PositionSignalement & { _id: string }) => { - changes.positions - ? addMarker({ - id: position._id, - isMapMarker: true, - isDisabled: true, - color: "warning", - label: `${position.type} - ${nom}`, - longitude: position.point.coordinates[0], - latitude: position.point.coordinates[1], - type: position.type as unknown as Position.type, - }) - : removeMarker(position._id); - }); - } - - return () => { - disableMarkers(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [positions, changes.positions]); - - const handleAcceptChange = (key: string, value: any) => { - setEditorValue({ ...editorValue, [key]: value }); - setChanges({ ...changes, [key]: false }); - }; - - const handleRefuseChange = (key: string) => { - setChanges({ ...changes, [key]: false }); - }; - - return ( - - - {refsInitialized && - changes.nom && - ReactDOM.createPortal( - handleAcceptChange("nom", nom)} - onRefuse={() => handleRefuseChange("nom")} - > - {nom} - , - refs.nom.current - )} - {refsInitialized && - changes.positions && - ReactDOM.createPortal( - handleAcceptChange("positions", positions)} - onRefuse={() => handleRefuseChange("positions")} - > - - - Type - -
- Latitude - Longitude -
- - {markers - .filter(({ isMapMarker }) => isMapMarker) - .map((marker) => ( - - ))} - - , - refs.positions.current - )} - {refsInitialized && - changes.parcelles && - ReactDOM.createPortal( - handleAcceptChange("parcelles", parcelles)} - onRefuse={() => handleRefuseChange("parcelles")} - > - {parcelles.map((parcelle) => ( - - {parcelle} - - ))} - , - refs.parcelles.current - )} - - ); -} - -export default SignalementUpdateToponyme; diff --git a/components/signalement/voie/signalement-update-voie.tsx b/components/signalement/voie/signalement-update-voie.tsx deleted file mode 100644 index 76efc5791..000000000 --- a/components/signalement/voie/signalement-update-voie.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Pane, Paragraph } from "evergreen-ui"; -import React, { useEffect, useRef, useState } from "react"; -import ReactDOM from "react-dom"; -import { CommuneType } from "@/types/commune"; -import SignalementCard from "../signalement-card"; -import VoieEditor from "@/components/bal/voie-editor"; -import { Signalement } from "@/lib/openapi-signalement"; -import { Voie } from "@/lib/openapi-api-bal"; - -interface SignalementUpdateVoieProps { - signalement: Signalement; - existingLocation: Voie; - handleSubmit: () => Promise; - handleClose: () => void; - commune: CommuneType; -} - -function SignalementUpdateVoie({ - signalement, - existingLocation, - handleSubmit, - handleClose, -}: SignalementUpdateVoieProps) { - const { nom } = signalement.changesRequested; - const formInputRef = useRef(null); - - const [changes, setChanges] = useState({ - nom: nom !== existingLocation.nom, - }); - const [refsInitialized, setRefsInitialized] = useState(false); - const [editorValue, setEditorValue] = useState(existingLocation); - - useEffect(() => { - if (formInputRef.current) { - changes.nom - ? (formInputRef.current.style.border = "solid #f3b346 2px") - : (formInputRef.current.style.border = "none"); - setRefsInitialized(true); - } - }, [formInputRef, changes]); - - const handleAcceptChange = (key: string, value: any) => { - setEditorValue({ ...editorValue, [key]: value }); - setChanges({ ...changes, [key]: false }); - }; - - const handleRefuseChange = (key: string) => { - setChanges({ ...changes, [key]: false }); - }; - - return ( - - - {refsInitialized && - changes.nom && - ReactDOM.createPortal( - handleAcceptChange("nom", nom)} - onRefuse={() => handleRefuseChange("nom")} - > - {nom} - , - formInputRef.current - )} - - ); -} - -export default SignalementUpdateVoie; diff --git a/components/trash/list/items-deleted-list.tsx b/components/trash/list/items-deleted-list.tsx index 345a56500..cbc714859 100644 --- a/components/trash/list/items-deleted-list.tsx +++ b/components/trash/list/items-deleted-list.tsx @@ -17,6 +17,8 @@ interface ItemsListDeleteProps { onRemoveNumeros?: (item: any) => void; } +const fuseOptions = { keys: ["nom"] }; + function ItemsListDelete({ itemsDeleted, model, @@ -24,7 +26,7 @@ function ItemsListDelete({ onRemove, onRemoveNumeros, }: ItemsListDeleteProps) { - const [filtered, setFilter] = useFuse(itemsDeleted, 200, { keys: ["nom"] }); + const [filtered, setFilter] = useFuse(itemsDeleted, 200, fuseOptions); const scrollableItems = useMemo( () => sortBy(filtered, (v) => normalizeSort(v.nom)), diff --git a/components/trash/restore-voie/list-numeros-deleted.tsx b/components/trash/restore-voie/list-numeros-deleted.tsx index dd590f7f7..51bd35e28 100644 --- a/components/trash/restore-voie/list-numeros-deleted.tsx +++ b/components/trash/restore-voie/list-numeros-deleted.tsx @@ -16,14 +16,16 @@ interface ListNumerosDeletedProps { setSelectedNumerosIds: Dispatch>; } +const fuseOptions = { + keys: ["numero"], +}; + function ListNumerosDeleted({ numeros, selectedNumerosIds, setSelectedNumerosIds, }: ListNumerosDeletedProps) { - const [filtered, setFilter] = useFuse(numeros, 200, { - keys: ["numero"], - }); + const [filtered, setFilter] = useFuse(numeros, 200, fuseOptions); const scrollableItems = useMemo( () => diff --git a/components/voie/numeros-list.tsx b/components/voie/numeros-list.tsx index 06ac9e439..46b67892f 100644 --- a/components/voie/numeros-list.tsx +++ b/components/voie/numeros-list.tsx @@ -40,6 +40,10 @@ interface NumerosListProps { handleEditing: (id?: string) => void; } +const fuseOptions = { + keys: ["numeroComplet"], +}; + function NumerosList({ token = null, voieId, @@ -63,9 +67,7 @@ function NumerosList({ const [isDisabled, setIsDisabled] = useState(false); - const [filtered, setFilter] = useFuse(numeros, 200, { - keys: ["numeroComplet"], - }); + const [filtered, setFilter] = useFuse(numeros, 200, fuseOptions); const scrollableItems = useMemo( () => diff --git a/contexts/layout.tsx b/contexts/layout.tsx index e09a0650c..e2c68d3ca 100644 --- a/contexts/layout.tsx +++ b/contexts/layout.tsx @@ -26,6 +26,8 @@ interface LayoutContextType { ) => () => Promise; toasts: Toast[]; pushToast: ({ intent, title, message }: Toast) => void; + breadcrumbs?: React.ReactNode; + setBreadcrumbs: (value: React.ReactNode) => void; } const LayoutContext = React.createContext(null); @@ -35,6 +37,7 @@ export function LayoutContextProvider(props: ChildrenProps) { const [isMapFullscreen, setIsMapFullscreen] = useState(false); const [isClientSide, setIsClientSide] = useState(false); const [toasts, setToasts] = useState([]); + const [breadcrumbs, setBreadcrumbs] = useState(); useEffect(() => { setIsClientSide(true); @@ -42,6 +45,9 @@ export function LayoutContextProvider(props: ChildrenProps) { useEffect(() => { const lastToast = toasts[toasts.length - 1]; + if (!lastToast) { + return; + } const timeoutDuration = lastToast?.duration || TOAST_DURATION; const timeout = setTimeout(() => { setToasts((toasts) => toasts.slice(1)); @@ -76,7 +82,7 @@ export function LayoutContextProvider(props: ChildrenProps) { } } }, - [setToasts] + [] ); const pushToast = useCallback( @@ -94,8 +100,10 @@ export function LayoutContextProvider(props: ChildrenProps) { toaster, pushToast, toasts, + breadcrumbs, + setBreadcrumbs, }), - [isMapFullscreen, isMobile, pushToast, toaster, toasts] + [isMapFullscreen, isMobile, pushToast, toaster, toasts, breadcrumbs] ); return ( diff --git a/contexts/map.tsx b/contexts/map.tsx index fd3a52be6..5606ed02a 100644 --- a/contexts/map.tsx +++ b/contexts/map.tsx @@ -27,6 +27,9 @@ interface MapContextType { setIsCadastreDisplayed: React.Dispatch>; balTilesUrl: string; isMapLoaded: boolean; + showTilesLayers: (show?: boolean) => void; + showToponymes: boolean; + setShowToponymes: React.Dispatch>; } const MapContext = React.createContext(null); @@ -47,6 +50,7 @@ export const SOURCE_TILE_ID = "tiles"; export function MapContextProvider(props: ChildrenProps) { const [map, setMap] = useState(null); + const [showToponymes, setShowToponymes] = useState(true); const [style, setStyle] = useState(defaultStyle); const [viewport, setViewport] = useState>(defaultViewport); const [isCadastreDisplayed, setIsCadastreDisplayed] = @@ -100,6 +104,26 @@ export function MapContextProvider(props: ChildrenProps) { } }, []); + const showTilesLayers = useCallback( + (show = true) => { + if (map && isMapLoaded) { + const tilesLayers = map + .getStyle() + ?.layers.filter((layer) => (layer as any).source === SOURCE_TILE_ID) + .map((layer) => layer.id); + + tilesLayers?.forEach((layerId) => { + map.setLayoutProperty( + layerId, + "visibility", + show ? "visible" : "none" + ); + }); + } + }, + [map, isMapLoaded] + ); + const value = useMemo( () => ({ map, @@ -117,6 +141,9 @@ export function MapContextProvider(props: ChildrenProps) { setIsCadastreDisplayed, balTilesUrl, isMapLoaded, + showTilesLayers, + showToponymes, + setShowToponymes, }), [ map, @@ -129,6 +156,9 @@ export function MapContextProvider(props: ChildrenProps) { isCadastreDisplayed, balTilesUrl, isMapLoaded, + showTilesLayers, + showToponymes, + setShowToponymes, ] ); diff --git a/contexts/markers.tsx b/contexts/markers.tsx index eac9390af..034fd835d 100644 --- a/contexts/markers.tsx +++ b/contexts/markers.tsx @@ -1,4 +1,10 @@ -import React, { useState, useCallback, useContext, useMemo } from "react"; +import React, { + useState, + useCallback, + useContext, + useMemo, + ReactNode, +} from "react"; import { ObjectId } from "bson"; import MapContext from "@/contexts/map"; @@ -7,7 +13,8 @@ import { ChildrenProps } from "@/types/context"; export interface Marker { id?: string; - label?: string; + tooltip?: string | ReactNode; + label?: string | ReactNode; type?: Position.type; latitude?: number; longitude?: number; @@ -15,6 +22,7 @@ export interface Marker { onClick?: () => void; color?: string; isMapMarker?: boolean; + showTooltip?: boolean; } interface MarkersContextType { @@ -74,7 +82,7 @@ export function MarkersContextProvider(props: ChildrenProps) { setMarkers((markers) => { return markers.map((marker) => { if (marker.id === markerId) { - return { id: markerId, ...data }; + return { id: markerId, ...marker, ...data }; } return marker; diff --git a/contexts/parcelles.tsx b/contexts/parcelles.tsx index 59da82d08..e9b957f86 100644 --- a/contexts/parcelles.tsx +++ b/contexts/parcelles.tsx @@ -19,46 +19,83 @@ import MapContext from "@/contexts/map"; import BalDataContext from "./bal-data"; interface ParcellesContextType { - selectedParcelles: string[]; - setSelectedParcelles: (value: string[]) => void; + highlightedParcelles: string[]; + setHighlightedParcelles: React.Dispatch>; isParcelleSelectionEnabled: boolean; - setIsParcelleSelectionEnabled: (value: boolean) => void; + setIsParcelleSelectionEnabled: React.Dispatch>; hoveredParcelle: { id: string; featureId?: string } | null; handleHoveredParcelle: ( value: { id: string; featureId?: string } | null ) => void; handleParcelle: (value: string) => void; + setShowSelectedParcelles?: React.Dispatch>; + handleSetFeatureState: ( + featureId: string, + state: { [key: string]: any } + ) => void; + isDiffMode?: boolean; + setIsDiffMode?: React.Dispatch>; + isCadastreVisible?: boolean; } const ParcellesContext = React.createContext(null); -function getHoveredFeatureId(map: MaplibreMap, id: string): string | undefined { +function getFeatureId(map: MaplibreMap, id: string): string | undefined { const [feature] = map.querySourceFeatures(CADASTRE_SOURCE, { sourceLayer: CADASTRE_SOURCE_LAYER.PARCELLES, filter: ["==", ["get", "id"], id], }); + return (feature?.id as string) || undefined; } export function ParcellesContextProvider(props: ChildrenProps) { const { map, isCadastreDisplayed, isStyleLoaded } = useContext(MapContext); - const { baseLocale, parcelles } = useContext(BalDataContext); - + const { baseLocale, parcelles: selectedParcelles } = + useContext(BalDataContext); + const [showSelectedParcelles, setShowSelectedParcelles] = + useState(true); + const [isDiffMode, setIsDiffMode] = useState(false); + const [isCadastreVisible, setIsCadastreVisible] = useState(false); const [hoveredParcelle, setHoveredParcelle] = useState<{ id: string; featureId?: string; } | null>(null); const [isParcelleSelectionEnabled, setIsParcelleSelectionEnabled] = useState(false); - const [selectedParcelles, setSelectedParcelles] = useState([]); + const [highlightedParcelles, setHighlightedParcelles] = useState( + [] + ); const prevHoveredParcelle = useRef(); + useEffect(() => { + const isCadastreVisibleCb = (e) => { + if (e.isSourceLoaded && e.sourceId === CADASTRE_SOURCE) { + setTimeout(() => { + setIsCadastreVisible(true); + }, 100); + } + }; + + if (isCadastreDisplayed && map) { + map.on("sourcedata", isCadastreVisibleCb); + } else { + setIsCadastreVisible(false); + } + + return () => { + if (map) { + map.off("sourcedata", isCadastreVisibleCb); + } + }; + }, [isCadastreDisplayed, map]); + const handleHoveredParcelle = useCallback( (hovered: { id: string; featureId?: string } | null) => { if (map && hovered) { const featureId: string | undefined = - hovered.featureId || getHoveredFeatureId(map, hovered.id); + hovered.featureId || getFeatureId(map, hovered.id); if (!hovered.featureId && isCadastreDisplayed) { // Handle parcelle from side menu @@ -93,12 +130,33 @@ export function ParcellesContextProvider(props: ChildrenProps) { [map, isCadastreDisplayed] ); + const handleSetFeatureState = useCallback( + (parcelleId: string, state: { [key: string]: boolean }) => { + if (map) { + const featureId = getFeatureId(map, parcelleId); + if (!featureId) { + return; + } + + map.setFeatureState( + { + source: CADASTRE_SOURCE, + sourceLayer: CADASTRE_SOURCE_LAYER.PARCELLES, + id: featureId, + }, + state + ); + } + }, + [map] + ); + const handleParcelle = useCallback( (parcelle: string) => { if (isParcelleSelectionEnabled) { - setSelectedParcelles((parcelles: string[]) => { - if (selectedParcelles.includes(parcelle)) { - return selectedParcelles.filter((id) => id !== parcelle); + setHighlightedParcelles((parcelles: string[]) => { + if (parcelles.includes(parcelle)) { + return parcelles.filter((id) => id !== parcelle); } return [...parcelles, parcelle]; @@ -106,7 +164,7 @@ export function ParcellesContextProvider(props: ChildrenProps) { handleHoveredParcelle(null); } }, - [selectedParcelles, isParcelleSelectionEnabled, handleHoveredParcelle] + [isParcelleSelectionEnabled, handleHoveredParcelle] ); const toggleCadastreVisibility = useCallback(() => { @@ -120,12 +178,10 @@ export function ParcellesContextProvider(props: ChildrenProps) { }, [map, isCadastreDisplayed]); const filterSelectedParcelles = useCallback(() => { - if (parcelles.length > 0) { - const exps: ExpressionSpecification[] = parcelles.map((id: string) => [ - "==", - ["get", "id"], - id, - ]); + if (selectedParcelles.length > 0 && showSelectedParcelles) { + const exps: ExpressionSpecification[] = selectedParcelles.map( + (id: string) => ["==", ["get", "id"], id] + ); map.setFilter(CADASTRE_LAYER.PARCELLES_SELECTED, ["any", ...exps]); } else { map.setFilter(CADASTRE_LAYER.PARCELLES_SELECTED, [ @@ -134,16 +190,21 @@ export function ParcellesContextProvider(props: ChildrenProps) { "", ]); } - }, [map, parcelles]); + }, [map, selectedParcelles, showSelectedParcelles]); const filterHighlightedParcelles = useCallback(() => { - if (parcelles.length > 0) { - const exps: ExpressionSpecification[] = selectedParcelles.map((id) => [ + if (selectedParcelles.length > 0) { + const exps: ExpressionSpecification[] = highlightedParcelles.map((id) => [ "==", ["get", "id"], id, ]); - map.setFilter(CADASTRE_LAYER.PARCELLE_HIGHLIGHTED, ["any", ...exps]); + map.setFilter( + isDiffMode + ? CADASTRE_LAYER.PARCELLE_HIGHLIGHTED_DIFF_MODE + : CADASTRE_LAYER.PARCELLE_HIGHLIGHTED, + ["any", ...exps] + ); } else { map.setFilter(CADASTRE_LAYER.PARCELLE_HIGHLIGHTED, [ "==", @@ -151,7 +212,7 @@ export function ParcellesContextProvider(props: ChildrenProps) { "", ]); } - }, [map, isParcelleSelectionEnabled, selectedParcelles]); + }, [map, selectedParcelles, highlightedParcelles, isDiffMode]); const reloadParcellesLayers = useCallback(() => { // Toggle all cadastre layers visiblity @@ -217,7 +278,7 @@ export function ParcellesContextProvider(props: ChildrenProps) { // Reset isStyleLoaded when selection is disabled useEffect(() => { if (!isParcelleSelectionEnabled && isStyleLoaded) { - setSelectedParcelles([]); + setHighlightedParcelles([]); } }, [isParcelleSelectionEnabled, isStyleLoaded]); @@ -229,20 +290,31 @@ export function ParcellesContextProvider(props: ChildrenProps) { const value = useMemo( () => ({ - selectedParcelles, - setSelectedParcelles, + highlightedParcelles, + setHighlightedParcelles, isParcelleSelectionEnabled, setIsParcelleSelectionEnabled, handleParcelle, hoveredParcelle, handleHoveredParcelle, + setShowSelectedParcelles, + handleSetFeatureState, + isDiffMode, + setIsDiffMode, + isCadastreVisible, }), [ - selectedParcelles, + highlightedParcelles, + setHighlightedParcelles, isParcelleSelectionEnabled, handleParcelle, hoveredParcelle, handleHoveredParcelle, + setShowSelectedParcelles, + handleSetFeatureState, + isDiffMode, + setIsDiffMode, + isCadastreVisible, ] ); diff --git a/contexts/signalement.tsx b/contexts/signalement.tsx index 781af30d0..bbca8f617 100644 --- a/contexts/signalement.tsx +++ b/contexts/signalement.tsx @@ -7,10 +7,7 @@ import React, { } from "react"; import { BaseLocale } from "@/lib/openapi-api-bal"; -import { - Signalement, - DefaultService as SignalementService, -} from "@/lib/openapi-signalement"; +import { Signalement, SignalementsService } from "@/lib/openapi-signalement"; import { ChildrenProps } from "@/types/context"; import BalDataContext from "./bal-data"; import TokenContext from "./token"; @@ -29,16 +26,25 @@ export function SignalementContextProvider(props: ChildrenProps) { const { token } = useContext(TokenContext); const fetchSignalements = useCallback(async () => { - const signalements = await SignalementService.getSignalementsByCodeCommune( - baseLocale.commune + const paginatedSignalements = await SignalementsService.getSignalements( + undefined, + undefined, + [Signalement.status.PENDING], + undefined, + undefined, + [baseLocale.commune] ); - setSignalements(signalements); + setSignalements(paginatedSignalements.data); }, [baseLocale.commune]); useEffect(() => { - const isSignalementEnabled = Boolean( - process.env.NEXT_PUBLIC_API_SIGNALEMENT - ); + const signalementWhiteList = + process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST?.split(",") || []; + + const isSignalementEnabled = + Boolean(process.env.NEXT_PUBLIC_API_SIGNALEMENT) && + signalementWhiteList.includes(baseLocale.commune); + if (!isSignalementEnabled) { return; } diff --git a/hooks/fuse.ts b/hooks/fuse.ts index 3444d6105..7e52a7cf2 100644 --- a/hooks/fuse.ts +++ b/hooks/fuse.ts @@ -30,6 +30,13 @@ function useFuse(source, delay, options, initialValue = null) { setFiltered(source); } } + + if (value) { + const results = fuse.search(value); + setFiltered(results.map((result) => result.item)); + } else { + setFiltered(source); + } }, delay); return [filtered, debouncedCallback]; diff --git a/hooks/useSignalementMapDiff.ts b/hooks/useSignalementMapDiff.ts new file mode 100644 index 000000000..c53bb3aa9 --- /dev/null +++ b/hooks/useSignalementMapDiff.ts @@ -0,0 +1,181 @@ +import { parcelleDiff } from "@/components/signalement/signalement-diff/signalement-parcelle-diff"; +import { positionDiff } from "@/components/signalement/signalement-diff/signalement-position-diff"; +import { signalementTypeMap } from "@/components/signalement/signalement-type-badge"; +import MapContext from "@/contexts/map"; +import MarkersContext from "@/contexts/markers"; +import ParcellesContext from "@/contexts/parcelles"; +import { Signalement } from "@/lib/openapi-signalement"; +import { getPositionName } from "@/lib/positions-types-list"; +import { ActiveCardEnum, SignalementDiff } from "@/lib/utils/signalement"; +import { useContext, useEffect, useState } from "react"; + +export type SignalementMapDiffExistingLocation = { + positions: any[]; + parcelles: string[]; +}; + +export type SignalementMapDiffChangesRequested = { + positions: any[]; + parcelles: string[]; +}; + +const mapInitialPositions = (positions: any[]) => + positions.map((position) => ({ + ...position, + color: + signalementTypeMap[Signalement.type.LOCATION_TO_CREATE].foregroundColor, + })); + +export function useSignalementMapDiff( + existingLocation: SignalementMapDiffExistingLocation, + changesRequested: SignalementMapDiffChangesRequested +) { + const { positions, parcelles } = changesRequested; + const { positions: existingPositions, parcelles: existingParcelles } = + existingLocation; + + const [activeCard, setActiveCard] = useState( + ActiveCardEnum.CHANGES + ); + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { map, isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { + isCadastreVisible, + setHighlightedParcelles, + setShowSelectedParcelles, + handleSetFeatureState, + setIsDiffMode, + } = useContext(ParcellesContext); + + const [positionsToDisplay, setPositionsToDisplay] = useState( + mapInitialPositions(positions) + ); + + useEffect(() => { + setPositionsToDisplay(mapInitialPositions(positions)); + }, [positions]); + + useEffect(() => { + if (isStyleLoaded) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + map, + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (positionsToDisplay?.length > 0) { + positionsToDisplay.forEach((position) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: position.color, + label: getPositionName(position.type), + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positionsToDisplay, addMarker, disableMarkers]); + + useEffect(() => { + if (!isCadastreVisible) { + return; + } + + switch (activeCard) { + case ActiveCardEnum.INITIAL: + setPositionsToDisplay( + existingPositions.map((position) => ({ + ...position, + color: "grey", + })) + ); + + if (existingParcelles?.length > 0) { + setHighlightedParcelles(existingParcelles); + existingParcelles.forEach((parcelle) => { + handleSetFeatureState(parcelle, { + diff: SignalementDiff.UNCHANGED, + }); + }); + } else { + setHighlightedParcelles([]); + } + break; + case ActiveCardEnum.CHANGES: + setPositionsToDisplay( + positionDiff(positions, existingPositions as any).map((position) => ({ + ...position, + color: + position.diff === SignalementDiff.DELETED + ? "orange" + : position.diff === SignalementDiff.NEW + ? "teal" + : Array.isArray(position.diff) && + position.diff[0] !== position.diff[1] + ? "purple" + : "grey", + })) + ); + const _parcelleDiff = parcelleDiff(parcelles, existingParcelles); + setHighlightedParcelles(_parcelleDiff.map(({ parcelle }) => parcelle)); + _parcelleDiff.forEach(({ parcelle, diff }) => { + handleSetFeatureState(parcelle, { diff }); + }); + break; + case ActiveCardEnum.FINAL: + setPositionsToDisplay( + positions.map((position) => ({ + ...position, + color: "grey", + })) + ); + if (parcelles?.length > 0) { + setHighlightedParcelles(parcelles); + parcelles.forEach((parcelle) => { + handleSetFeatureState(parcelle, { + diff: SignalementDiff.UNCHANGED, + }); + }); + } else { + setHighlightedParcelles([]); + } + break; + } + }, [ + isCadastreVisible, + activeCard, + parcelles, + positions, + existingParcelles, + existingPositions, + handleSetFeatureState, + setHighlightedParcelles, + ]); + + return { + activeCard, + setActiveCard, + }; +} diff --git a/lib/openapi-api-bal/index.ts b/lib/openapi-api-bal/index.ts index 8c3a0bf0d..54d22ee42 100644 --- a/lib/openapi-api-bal/index.ts +++ b/lib/openapi-api-bal/index.ts @@ -40,6 +40,7 @@ export type { UpdateBaseLocaleDTO } from './models/UpdateBaseLocaleDTO'; export type { UpdateBatchNumeroChangeDTO } from './models/UpdateBatchNumeroChangeDTO'; export type { UpdateBatchNumeroDTO } from './models/UpdateBatchNumeroDTO'; export type { UpdateNumeroDTO } from './models/UpdateNumeroDTO'; +export { UpdateSignalementDTO } from './models/UpdateSignalementDTO'; export type { UpdateToponymeDTO } from './models/UpdateToponymeDTO'; export { UpdateVoieDTO } from './models/UpdateVoieDTO'; export type { ValidatePinCodeDTO } from './models/ValidatePinCodeDTO'; @@ -51,6 +52,7 @@ export { CommuneService } from './services/CommuneService'; export { ExportCsvService } from './services/ExportCsvService'; export { HabilitationService } from './services/HabilitationService'; export { NumerosService } from './services/NumerosService'; +export { SignalementsService } from './services/SignalementsService'; export { StatsService } from './services/StatsService'; export { TilesService } from './services/TilesService'; export { ToponymesService } from './services/ToponymesService'; diff --git a/lib/openapi-api-bal/models/UpdateSignalementDTO.ts b/lib/openapi-api-bal/models/UpdateSignalementDTO.ts new file mode 100644 index 000000000..26d25999c --- /dev/null +++ b/lib/openapi-api-bal/models/UpdateSignalementDTO.ts @@ -0,0 +1,22 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type UpdateSignalementDTO = { + ids: Array; + status: UpdateSignalementDTO.status; +}; + +export namespace UpdateSignalementDTO { + + export enum status { + PENDING = 'PENDING', + IGNORED = 'IGNORED', + PROCESSED = 'PROCESSED', + EXPIRED = 'EXPIRED', + } + + +} + diff --git a/lib/openapi-api-bal/services/SignalementsService.ts b/lib/openapi-api-bal/services/SignalementsService.ts new file mode 100644 index 000000000..2761038b5 --- /dev/null +++ b/lib/openapi-api-bal/services/SignalementsService.ts @@ -0,0 +1,35 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { UpdateSignalementDTO } from '../models/UpdateSignalementDTO'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SignalementsService { + + /** + * Update signalements + * @param baseLocaleId + * @param requestBody + * @returns boolean + * @throws ApiError + */ + public static updateSignalements( + baseLocaleId: string, + requestBody: UpdateSignalementDTO, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/v2/signalements/{baseLocaleId}', + path: { + 'baseLocaleId': baseLocaleId, + }, + body: requestBody, + mediaType: 'application/json', + }); + } + +} diff --git a/lib/openapi-signalement/index.ts b/lib/openapi-signalement/index.ts index 24024d2a3..ec881165d 100644 --- a/lib/openapi-signalement/index.ts +++ b/lib/openapi-signalement/index.ts @@ -8,17 +8,30 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { Author } from './models/Author'; -export type { AuthorDTO } from './models/AuthorDTO'; -export type { ChangesRequested } from './models/ChangesRequested'; +export type { AuthorInput } from './models/AuthorInput'; +export type { Client } from './models/Client'; +export type { CreateClientDTO } from './models/CreateClientDTO'; export { CreateSignalementDTO } from './models/CreateSignalementDTO'; +export { CreateSourceDTO } from './models/CreateSourceDTO'; +export type { DeleteNumeroChangesRequestedDTO } from './models/DeleteNumeroChangesRequestedDTO'; export { ExistingLocation } from './models/ExistingLocation'; export { ExistingNumero } from './models/ExistingNumero'; export { ExistingToponyme } from './models/ExistingToponyme'; export { ExistingVoie } from './models/ExistingVoie'; -export type { ObjectId } from './models/ObjectId'; +export type { NumeroChangesRequestedDTO } from './models/NumeroChangesRequestedDTO'; +export type { PaginatedSignalementsDTO } from './models/PaginatedSignalementsDTO'; export type { Point } from './models/Point'; export { Position } from './models/Position'; +export type { PositionCoordinatesDTO } from './models/PositionCoordinatesDTO'; +export { PositionDTO } from './models/PositionDTO'; export { Signalement } from './models/Signalement'; -export type { UpdateSignalementDTO } from './models/UpdateSignalementDTO'; +export type { SignalementStatsDTO } from './models/SignalementStatsDTO'; +export { Source } from './models/Source'; +export type { ToponymeChangesRequestedDTO } from './models/ToponymeChangesRequestedDTO'; +export { UpdateSignalementDTO } from './models/UpdateSignalementDTO'; +export type { VoieChangesRequestedDTO } from './models/VoieChangesRequestedDTO'; -export { DefaultService } from './services/DefaultService'; +export { ClientsService } from './services/ClientsService'; +export { SignalementsService } from './services/SignalementsService'; +export { SourcesService } from './services/SourcesService'; +export { StatsService } from './services/StatsService'; diff --git a/lib/openapi-signalement/models/Author.ts b/lib/openapi-signalement/models/Author.ts index 820e565d8..404f246c0 100644 --- a/lib/openapi-signalement/models/Author.ts +++ b/lib/openapi-signalement/models/Author.ts @@ -4,8 +4,6 @@ /* eslint-disable */ export type Author = { - firstName?: string | null; - lastName?: string | null; - email: string; + email?: string | null; }; diff --git a/lib/openapi-signalement/models/AuthorDTO.ts b/lib/openapi-signalement/models/AuthorInput.ts similarity index 65% rename from lib/openapi-signalement/models/AuthorDTO.ts rename to lib/openapi-signalement/models/AuthorInput.ts index 1d6845c46..19c88c4df 100644 --- a/lib/openapi-signalement/models/AuthorDTO.ts +++ b/lib/openapi-signalement/models/AuthorInput.ts @@ -3,9 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -export type AuthorDTO = { - firstName?: string | null; - lastName?: string | null; +export type AuthorInput = { email?: string | null; }; diff --git a/lib/openapi-signalement/models/ChangesRequested.ts b/lib/openapi-signalement/models/ChangesRequested.ts deleted file mode 100644 index 2f5bca43f..000000000 --- a/lib/openapi-signalement/models/ChangesRequested.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ - -import type { Position } from './Position'; - -export type ChangesRequested = { - numero: number; - suffixe?: string | null; - positions?: Array | null; - parcelles?: Array | null; - nomVoie?: string | null; - nom?: string | null; -}; - diff --git a/lib/openapi-signalement/models/Client.ts b/lib/openapi-signalement/models/Client.ts new file mode 100644 index 000000000..fdfd785e8 --- /dev/null +++ b/lib/openapi-signalement/models/Client.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Client = { + id: string; + createdAt: string; + updatedAt: string; + deletedAt?: string | null; + nom: string; + processedSignalements?: Array | null; +}; + diff --git a/lib/openapi-signalement/models/ObjectId.ts b/lib/openapi-signalement/models/CreateClientDTO.ts similarity index 73% rename from lib/openapi-signalement/models/ObjectId.ts rename to lib/openapi-signalement/models/CreateClientDTO.ts index 4515196d8..b432e841d 100644 --- a/lib/openapi-signalement/models/ObjectId.ts +++ b/lib/openapi-signalement/models/CreateClientDTO.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ -export type ObjectId = { +export type CreateClientDTO = { + nom: string; }; diff --git a/lib/openapi-signalement/models/CreateSignalementDTO.ts b/lib/openapi-signalement/models/CreateSignalementDTO.ts index e459457e0..deeb67698 100644 --- a/lib/openapi-signalement/models/CreateSignalementDTO.ts +++ b/lib/openapi-signalement/models/CreateSignalementDTO.ts @@ -3,15 +3,21 @@ /* tslint:disable */ /* eslint-disable */ -import type { AuthorDTO } from './AuthorDTO'; -import type { ExistingLocation } from './ExistingLocation'; +import type { AuthorInput } from './AuthorInput'; +import type { DeleteNumeroChangesRequestedDTO } from './DeleteNumeroChangesRequestedDTO'; +import type { ExistingNumero } from './ExistingNumero'; +import type { ExistingToponyme } from './ExistingToponyme'; +import type { ExistingVoie } from './ExistingVoie'; +import type { NumeroChangesRequestedDTO } from './NumeroChangesRequestedDTO'; +import type { ToponymeChangesRequestedDTO } from './ToponymeChangesRequestedDTO'; +import type { VoieChangesRequestedDTO } from './VoieChangesRequestedDTO'; export type CreateSignalementDTO = { codeCommune: string; type: CreateSignalementDTO.type; - author?: AuthorDTO | null; - existingLocation?: ExistingLocation | null; - changesRequested: Record | null; + author?: AuthorInput | null; + existingLocation?: (ExistingNumero | ExistingVoie | ExistingToponyme) | null; + changesRequested: (NumeroChangesRequestedDTO | DeleteNumeroChangesRequestedDTO | ToponymeChangesRequestedDTO | VoieChangesRequestedDTO) | null; }; export namespace CreateSignalementDTO { @@ -20,7 +26,6 @@ export namespace CreateSignalementDTO { LOCATION_TO_UPDATE = 'LOCATION_TO_UPDATE', LOCATION_TO_DELETE = 'LOCATION_TO_DELETE', LOCATION_TO_CREATE = 'LOCATION_TO_CREATE', - OTHER = 'OTHER', } diff --git a/lib/openapi-signalement/models/CreateSourceDTO.ts b/lib/openapi-signalement/models/CreateSourceDTO.ts new file mode 100644 index 000000000..7d87e093d --- /dev/null +++ b/lib/openapi-signalement/models/CreateSourceDTO.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CreateSourceDTO = { + nom: string; + type: CreateSourceDTO.type; +}; + +export namespace CreateSourceDTO { + + export enum type { + PUBLIC = 'PUBLIC', + PRIVATE = 'PRIVATE', + } + + +} + diff --git a/lib/openapi-signalement/models/DeleteNumeroChangesRequestedDTO.ts b/lib/openapi-signalement/models/DeleteNumeroChangesRequestedDTO.ts new file mode 100644 index 000000000..b55342e49 --- /dev/null +++ b/lib/openapi-signalement/models/DeleteNumeroChangesRequestedDTO.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type DeleteNumeroChangesRequestedDTO = { + comment: string; +}; + diff --git a/lib/openapi-signalement/models/ExistingLocation.ts b/lib/openapi-signalement/models/ExistingLocation.ts index 40ee5ab7e..529f54521 100644 --- a/lib/openapi-signalement/models/ExistingLocation.ts +++ b/lib/openapi-signalement/models/ExistingLocation.ts @@ -5,6 +5,7 @@ export type ExistingLocation = { type: ExistingLocation.type; + banId?: string | null; }; export namespace ExistingLocation { diff --git a/lib/openapi-signalement/models/ExistingNumero.ts b/lib/openapi-signalement/models/ExistingNumero.ts index 7c6c112a3..5b06e85a1 100644 --- a/lib/openapi-signalement/models/ExistingNumero.ts +++ b/lib/openapi-signalement/models/ExistingNumero.ts @@ -9,10 +9,13 @@ import type { Position } from './Position'; export type ExistingNumero = { type: ExistingNumero.type; + banId?: string | null; numero: number; suffixe: string; position: Position; + parcelles?: Array | null; toponyme: (ExistingVoie | ExistingToponyme); + nomComplement?: string; }; export namespace ExistingNumero { diff --git a/lib/openapi-signalement/models/ExistingToponyme.ts b/lib/openapi-signalement/models/ExistingToponyme.ts index d53f56c3f..9bf30944f 100644 --- a/lib/openapi-signalement/models/ExistingToponyme.ts +++ b/lib/openapi-signalement/models/ExistingToponyme.ts @@ -3,9 +3,14 @@ /* tslint:disable */ /* eslint-disable */ +import type { Position } from './Position'; + export type ExistingToponyme = { type: ExistingToponyme.type; + banId?: string | null; nom: string; + position: Position; + parcelles?: Array | null; }; export namespace ExistingToponyme { diff --git a/lib/openapi-signalement/models/ExistingVoie.ts b/lib/openapi-signalement/models/ExistingVoie.ts index 6b10b8014..d211dcd29 100644 --- a/lib/openapi-signalement/models/ExistingVoie.ts +++ b/lib/openapi-signalement/models/ExistingVoie.ts @@ -5,6 +5,7 @@ export type ExistingVoie = { type: ExistingVoie.type; + banId?: string | null; nom: string; }; diff --git a/lib/openapi-signalement/models/NumeroChangesRequestedDTO.ts b/lib/openapi-signalement/models/NumeroChangesRequestedDTO.ts new file mode 100644 index 000000000..a2ff21f85 --- /dev/null +++ b/lib/openapi-signalement/models/NumeroChangesRequestedDTO.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PositionDTO } from './PositionDTO'; + +export type NumeroChangesRequestedDTO = { + numero: number; + suffixe?: string; + nomVoie: string; + nomComplement?: string; + parcelles: Array; + positions: Array; + comment?: string | null; +}; + diff --git a/lib/openapi-signalement/models/PaginatedSignalementsDTO.ts b/lib/openapi-signalement/models/PaginatedSignalementsDTO.ts new file mode 100644 index 000000000..2690ba1c4 --- /dev/null +++ b/lib/openapi-signalement/models/PaginatedSignalementsDTO.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type PaginatedSignalementsDTO = { + data: Array; + page: number; + limit: number; + total: number; +}; + diff --git a/lib/openapi-signalement/models/Position.ts b/lib/openapi-signalement/models/Position.ts index fe7410fc7..00dc717b4 100644 --- a/lib/openapi-signalement/models/Position.ts +++ b/lib/openapi-signalement/models/Position.ts @@ -21,7 +21,6 @@ export namespace Position { D_LIVRANCE_POSTALE = 'délivrance postale', PARCELLE = 'parcelle', SEGMENT = 'segment', - INCONNUE = 'inconnue', } diff --git a/lib/openapi-signalement/models/PositionCoordinatesDTO.ts b/lib/openapi-signalement/models/PositionCoordinatesDTO.ts new file mode 100644 index 000000000..b5b1db604 --- /dev/null +++ b/lib/openapi-signalement/models/PositionCoordinatesDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type PositionCoordinatesDTO = { + coordinates: Array; + type: string; +}; + diff --git a/lib/openapi-signalement/models/PositionDTO.ts b/lib/openapi-signalement/models/PositionDTO.ts new file mode 100644 index 000000000..02ddb6dd7 --- /dev/null +++ b/lib/openapi-signalement/models/PositionDTO.ts @@ -0,0 +1,28 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PositionCoordinatesDTO } from './PositionCoordinatesDTO'; + +export type PositionDTO = { + point: PositionCoordinatesDTO; + type: PositionDTO.type; +}; + +export namespace PositionDTO { + + export enum type { + ENTR_E = 'entrée', + B_TIMENT = 'bâtiment', + CAGE_D_ESCALIER = 'cage d’escalier', + LOGEMENT = 'logement', + SERVICE_TECHNIQUE = 'service technique', + D_LIVRANCE_POSTALE = 'délivrance postale', + PARCELLE = 'parcelle', + SEGMENT = 'segment', + } + + +} + diff --git a/lib/openapi-signalement/models/Signalement.ts b/lib/openapi-signalement/models/Signalement.ts index b8dc6c0a7..1e186187c 100644 --- a/lib/openapi-signalement/models/Signalement.ts +++ b/lib/openapi-signalement/models/Signalement.ts @@ -4,23 +4,29 @@ /* eslint-disable */ import type { Author } from './Author'; -import type { ChangesRequested } from './ChangesRequested'; +import type { Client } from './Client'; +import type { DeleteNumeroChangesRequestedDTO } from './DeleteNumeroChangesRequestedDTO'; import type { ExistingNumero } from './ExistingNumero'; import type { ExistingToponyme } from './ExistingToponyme'; import type { ExistingVoie } from './ExistingVoie'; -import type { ObjectId } from './ObjectId'; +import type { NumeroChangesRequestedDTO } from './NumeroChangesRequestedDTO'; +import type { Source } from './Source'; +import type { ToponymeChangesRequestedDTO } from './ToponymeChangesRequestedDTO'; +import type { VoieChangesRequestedDTO } from './VoieChangesRequestedDTO'; export type Signalement = { - _id: ObjectId; - _created: string; - _updated: string; - _deleted?: string | null; + id: string; + createdAt: string; + updatedAt: string; + deletedAt?: string | null; codeCommune: string; type: Signalement.type; author?: Author | null; existingLocation?: (ExistingNumero | ExistingVoie | ExistingToponyme) | null; - changesRequested: ChangesRequested; - processedAt?: string | null; + changesRequested: (NumeroChangesRequestedDTO | DeleteNumeroChangesRequestedDTO | ToponymeChangesRequestedDTO | VoieChangesRequestedDTO); + status?: Signalement.status | null; + source: Source; + processedBy?: Client | null; }; export namespace Signalement { @@ -29,7 +35,13 @@ export namespace Signalement { LOCATION_TO_UPDATE = 'LOCATION_TO_UPDATE', LOCATION_TO_DELETE = 'LOCATION_TO_DELETE', LOCATION_TO_CREATE = 'LOCATION_TO_CREATE', - OTHER = 'OTHER', + } + + export enum status { + PENDING = 'PENDING', + IGNORED = 'IGNORED', + PROCESSED = 'PROCESSED', + EXPIRED = 'EXPIRED', } diff --git a/lib/openapi-signalement/models/SignalementStatsDTO.ts b/lib/openapi-signalement/models/SignalementStatsDTO.ts new file mode 100644 index 000000000..d2f03c4bd --- /dev/null +++ b/lib/openapi-signalement/models/SignalementStatsDTO.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type SignalementStatsDTO = { + total: number; + fromSources: Record; + processedBy: Record; +}; + diff --git a/lib/openapi-signalement/models/Source.ts b/lib/openapi-signalement/models/Source.ts new file mode 100644 index 000000000..0a14a0ea1 --- /dev/null +++ b/lib/openapi-signalement/models/Source.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Source = { + id: string; + createdAt: string; + updatedAt: string; + deletedAt?: string | null; + nom: string; + type: Source.type; + signalements?: Array | null; +}; + +export namespace Source { + + export enum type { + PUBLIC = 'PUBLIC', + PRIVATE = 'PRIVATE', + } + + +} + diff --git a/lib/openapi-signalement/models/ToponymeChangesRequestedDTO.ts b/lib/openapi-signalement/models/ToponymeChangesRequestedDTO.ts new file mode 100644 index 000000000..a26418889 --- /dev/null +++ b/lib/openapi-signalement/models/ToponymeChangesRequestedDTO.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PositionDTO } from './PositionDTO'; + +export type ToponymeChangesRequestedDTO = { + nom: string; + parcelles: Array; + positions: Array; + comment?: string | null; +}; + diff --git a/lib/openapi-signalement/models/UpdateSignalementDTO.ts b/lib/openapi-signalement/models/UpdateSignalementDTO.ts index 892d5b165..0a5fbaeed 100644 --- a/lib/openapi-signalement/models/UpdateSignalementDTO.ts +++ b/lib/openapi-signalement/models/UpdateSignalementDTO.ts @@ -4,6 +4,18 @@ /* eslint-disable */ export type UpdateSignalementDTO = { - id: string; + status: UpdateSignalementDTO.status; }; +export namespace UpdateSignalementDTO { + + export enum status { + PENDING = 'PENDING', + IGNORED = 'IGNORED', + PROCESSED = 'PROCESSED', + EXPIRED = 'EXPIRED', + } + + +} + diff --git a/lib/openapi-signalement/models/VoieChangesRequestedDTO.ts b/lib/openapi-signalement/models/VoieChangesRequestedDTO.ts new file mode 100644 index 000000000..bb6c11451 --- /dev/null +++ b/lib/openapi-signalement/models/VoieChangesRequestedDTO.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type VoieChangesRequestedDTO = { + nom: string; + comment?: string | null; +}; + diff --git a/lib/openapi-signalement/services/ClientsService.ts b/lib/openapi-signalement/services/ClientsService.ts new file mode 100644 index 000000000..f27a9911a --- /dev/null +++ b/lib/openapi-signalement/services/ClientsService.ts @@ -0,0 +1,31 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Client } from '../models/Client'; +import type { CreateClientDTO } from '../models/CreateClientDTO'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class ClientsService { + + /** + * Create a new Client + * @param requestBody + * @returns Client + * @throws ApiError + */ + public static createClient( + requestBody: CreateClientDTO, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/clients', + body: requestBody, + mediaType: 'application/json', + }); + } + +} diff --git a/lib/openapi-signalement/services/DefaultService.ts b/lib/openapi-signalement/services/DefaultService.ts deleted file mode 100644 index 49bff5dd3..000000000 --- a/lib/openapi-signalement/services/DefaultService.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { CreateSignalementDTO } from '../models/CreateSignalementDTO'; -import type { Signalement } from '../models/Signalement'; -import type { UpdateSignalementDTO } from '../models/UpdateSignalementDTO'; - -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; - -export class DefaultService { - - /** - * Get all signalements for a given codeCommune - * @param codeCommune - * @returns any[] - * @throws ApiError - */ - public static getSignalementsByCodeCommune( - codeCommune: string, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/signalements/{codeCommune}', - path: { - 'codeCommune': codeCommune, - }, - }); - } - - /** - * Create a new signalement - * @param requestBody - * @returns Signalement - * @throws ApiError - */ - public static createSignalement( - requestBody: CreateSignalementDTO, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/signalements', - body: requestBody, - mediaType: 'application/json', - }); - } - - /** - * Update a given signalement - * @param requestBody - * @returns Signalement - * @throws ApiError - */ - public static updateSignalement( - requestBody: UpdateSignalementDTO, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'PUT', - url: '/signalements', - body: requestBody, - mediaType: 'application/json', - }); - } - -} diff --git a/lib/openapi-signalement/services/SignalementsService.ts b/lib/openapi-signalement/services/SignalementsService.ts new file mode 100644 index 000000000..72d3eaaf6 --- /dev/null +++ b/lib/openapi-signalement/services/SignalementsService.ts @@ -0,0 +1,111 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateSignalementDTO } from '../models/CreateSignalementDTO'; +import type { PaginatedSignalementsDTO } from '../models/PaginatedSignalementsDTO'; +import type { Signalement } from '../models/Signalement'; +import type { UpdateSignalementDTO } from '../models/UpdateSignalementDTO'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SignalementsService { + + /** + * Get signalements + * @param limit + * @param page + * @param status + * @param types + * @param sourceIds + * @param codeCommunes + * @returns PaginatedSignalementsDTO + * @throws ApiError + */ + public static getSignalements( + limit?: number, + page?: number, + status?: Array<'PENDING' | 'IGNORED' | 'PROCESSED' | 'EXPIRED'>, + types?: Array<'LOCATION_TO_UPDATE' | 'LOCATION_TO_DELETE' | 'LOCATION_TO_CREATE'>, + sourceIds?: Array, + codeCommunes?: Array, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/signalements', + query: { + 'limit': limit, + 'page': page, + 'status': status, + 'types': types, + 'sourceIds': sourceIds, + 'codeCommunes': codeCommunes, + }, + }); + } + + /** + * Create a new signalement + * @param requestBody + * @param sourceId + * @returns Signalement + * @throws ApiError + */ + public static createSignalement( + requestBody: CreateSignalementDTO, + sourceId?: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/signalements', + query: { + 'sourceId': sourceId, + }, + body: requestBody, + mediaType: 'application/json', + }); + } + + /** + * Get signalement by id + * @param idSignalement + * @returns Signalement + * @throws ApiError + */ + public static getSignalementById( + idSignalement: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/signalements/{idSignalement}', + path: { + 'idSignalement': idSignalement, + }, + }); + } + + /** + * Update a given signalement + * @param idSignalement + * @param requestBody + * @returns Signalement + * @throws ApiError + */ + public static updateSignalement( + idSignalement: string, + requestBody: UpdateSignalementDTO, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/signalements/{idSignalement}', + path: { + 'idSignalement': idSignalement, + }, + body: requestBody, + mediaType: 'application/json', + }); + } + +} diff --git a/lib/openapi-signalement/services/SourcesService.ts b/lib/openapi-signalement/services/SourcesService.ts new file mode 100644 index 000000000..edc8da977 --- /dev/null +++ b/lib/openapi-signalement/services/SourcesService.ts @@ -0,0 +1,85 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateSourceDTO } from '../models/CreateSourceDTO'; +import type { Source } from '../models/Source'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class SourcesService { + + /** + * Get all sources + * @param type + * @returns any[] + * @throws ApiError + */ + public static getSources( + type?: 'PUBLIC' | 'PRIVATE', + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/sources', + query: { + 'type': type, + }, + }); + } + + /** + * Create a new source + * @param requestBody + * @returns Source + * @throws ApiError + */ + public static createSource( + requestBody: CreateSourceDTO, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/sources', + body: requestBody, + mediaType: 'application/json', + }); + } + + /** + * Get source by id + * @param idSource + * @returns Source + * @throws ApiError + */ + public static getSourceById( + idSource: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/sources/{idSource}', + path: { + 'idSource': idSource, + }, + }); + } + + /** + * Get source by token + * @param token + * @returns Source + * @throws ApiError + */ + public static getSourceByToken( + token: string, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/sources/token/{token}', + path: { + 'token': token, + }, + }); + } + +} diff --git a/lib/openapi-signalement/services/StatsService.ts b/lib/openapi-signalement/services/StatsService.ts new file mode 100644 index 000000000..3f43e8f12 --- /dev/null +++ b/lib/openapi-signalement/services/StatsService.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { SignalementStatsDTO } from '../models/SignalementStatsDTO'; + +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; + +export class StatsService { + + /** + * Get stats + * @returns SignalementStatsDTO + * @throws ApiError + */ + public static getStats(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/stats', + }); + } + +} diff --git a/lib/utils/address.ts b/lib/utils/address.ts new file mode 100644 index 000000000..0311596c2 --- /dev/null +++ b/lib/utils/address.ts @@ -0,0 +1,33 @@ +import { computeCompletNumero } from "@/lib/utils/numero"; +import { CommuneType } from "@/types/commune"; + +export const getAddressPreview = ( + numero: string | number, + suffixe: string, + commune?: CommuneType, + toponyme?: string, + voie?: string +) => { + const completNumero = computeCompletNumero(numero, suffixe) || ""; + if (toponyme) { + return `${completNumero} ${voie}, ${toponyme}${ + commune ? ` - ${commune.nom} (${commune.code})` : "" + }`; + } + + if (voie) { + return `${completNumero} ${voie}${ + commune ? ` - ${commune.nom} (${commune.code})` : "" + }`; + } + + if (!voie && !toponyme) { + return `${completNumero}${ + commune ? ` - ${commune.nom} (${commune.code})` : "" + }`; + } + + return `${completNumero} ${voie}${ + commune ? ` - ${commune.nom} (${commune.code})` : "" + }`; +}; diff --git a/lib/utils/date.ts b/lib/utils/date.ts new file mode 100644 index 000000000..1253a213b --- /dev/null +++ b/lib/utils/date.ts @@ -0,0 +1,18 @@ +export const getDuration = (start: Date, end: Date = new Date()) => { + const duration = end.getTime() - start.getTime(); + const seconds = Math.floor(duration / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days} jour${days > 1 ? "s" : ""}`; + } + if (hours > 0) { + return `${hours} heure${hours > 1 ? "s" : ""}`; + } + if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? "s" : ""}`; + } + return `${seconds} seconde${seconds > 1 ? "s" : ""}`; +}; diff --git a/lib/utils/signalement.ts b/lib/utils/signalement.ts index bd22e29b6..5dac5ca3d 100644 --- a/lib/utils/signalement.ts +++ b/lib/utils/signalement.ts @@ -1,12 +1,31 @@ import { - ChangesRequested, + Numero, + Toponyme, + ToponymesService, + Voie, + VoiesService, +} from "../openapi-api-bal"; +import { ExistingLocation, ExistingNumero, ExistingToponyme, ExistingVoie, + NumeroChangesRequestedDTO, Signalement, } from "../openapi-signalement"; +export enum SignalementDiff { + NEW = "new", + DELETED = "deleted", + UNCHANGED = "unchanged", +} + +export enum ActiveCardEnum { + INITIAL = "initial", + CHANGES = "changes", + FINAL = "final", +} + export const getExistingLocationLabel = ( existingLocation: ExistingNumero | ExistingToponyme | ExistingVoie ) => { @@ -33,7 +52,9 @@ export const getExistingLocationLabel = ( return label; }; -const getRequestedLocationLabel = (changesRequested: ChangesRequested) => { +const getRequestedLocationLabel = ( + changesRequested: NumeroChangesRequestedDTO +) => { return `${changesRequested.numero} ${ changesRequested.suffixe ? `${changesRequested.suffixe} ` : "" }${changesRequested.nomVoie}`; @@ -45,36 +66,153 @@ export const getSignalementLabel = ( ) => { let label = ""; switch (signalement.type) { - case Signalement.type.LOCATION_TO_UPDATE: - label = `Demande de modification : ${getExistingLocationLabel( - signalement.existingLocation - )}${ - opts?.withoutDate - ? "" - : `- ${new Date(signalement._created).toLocaleDateString()}` - }`; - break; case Signalement.type.LOCATION_TO_CREATE: - label = `Demande de creation : ${getRequestedLocationLabel( - signalement.changesRequested + label = `${getRequestedLocationLabel( + signalement.changesRequested as NumeroChangesRequestedDTO )}${ opts?.withoutDate ? "" - : `- ${new Date(signalement._created).toLocaleDateString()}` + : ` - ${new Date(signalement.createdAt).toLocaleDateString()}` }`; break; - case Signalement.type.LOCATION_TO_DELETE: - label = `Demande de suppression : ${getExistingLocationLabel( - signalement.existingLocation - )}${ + default: + label = `${getExistingLocationLabel(signalement.existingLocation)}${ opts?.withoutDate ? "" - : `- ${new Date(signalement._created).toLocaleDateString()}` + : ` - ${new Date(signalement.createdAt).toLocaleDateString()}` }`; - break; - default: - label = "Autre demande"; } return label; }; + +export async function getExistingLocation( + signalement: Signalement, + voies: Voie[], + toponymes: Toponyme[] +) { + let existingLocation = null; + if (signalement.existingLocation.type === ExistingLocation.type.VOIE) { + existingLocation = voies.find((voie) => { + if ((signalement.existingLocation as ExistingVoie).banId) { + return ( + voie.banId === (signalement.existingLocation as ExistingVoie).banId + ); + } + + return voie.nom === (signalement.existingLocation as ExistingVoie).nom; + }); + } else if ( + signalement.existingLocation.type === ExistingLocation.type.TOPONYME + ) { + existingLocation = toponymes.find((toponyme) => { + if ((signalement.existingLocation as ExistingToponyme).banId) { + return ( + toponyme.banId === + (signalement.existingLocation as ExistingToponyme).banId + ); + } + + return ( + toponyme.nom === (signalement.existingLocation as ExistingToponyme).nom + ); + }); + } else if ( + signalement.existingLocation.type === ExistingLocation.type.NUMERO + ) { + const existingNumero = signalement.existingLocation as ExistingNumero; + if (existingNumero.toponyme.type === ExistingLocation.type.VOIE) { + const voie = voies.find((voie) => { + if (existingNumero.toponyme.banId) { + return voie.banId === existingNumero.toponyme.banId; + } + + return voie.nom === existingNumero.toponyme.nom; + }); + const numeros = await VoiesService.findVoieNumeros(voie.id); + existingLocation = numeros.find(({ numero, suffixe, banId }) => { + if (existingNumero.banId) { + return banId === existingNumero.banId; + } + + const existingLocationNumeroComplet = existingNumero.suffixe + ? `${existingNumero.numero}${existingNumero.suffixe}` + : `${existingNumero.numero}`; + const numeroComplet = suffixe ? `${numero}${suffixe}` : `${numero}`; + + return numeroComplet === existingLocationNumeroComplet; + }); + if (existingLocation) { + existingLocation.voie = voie; + if ((existingLocation as Numero).toponymeId) { + const toponyme = toponymes.find( + (toponyme) => + toponyme.id === (existingLocation as Numero).toponymeId + ); + (existingLocation as Numero).toponyme = toponyme; + } + } + } else { + const toponyme = toponymes.find((toponyme) => { + if (existingNumero.toponyme.banId) { + return toponyme.banId === existingNumero.toponyme.banId; + } + + return toponyme.nom === existingNumero.toponyme.nom; + }); + const numeros = await ToponymesService.findToponymeNumeros(toponyme.id); + existingLocation = numeros.find(({ numero, suffixe, banId }) => { + if (existingNumero.banId) { + return banId === existingNumero.banId; + } + + const existingLocationNumeroComplet = existingNumero.suffixe + ? `${existingNumero.numero}${existingNumero.suffixe}` + : `${existingNumero.numero}`; + const numeroComplet = suffixe ? `${numero}${suffixe}` : `${numero}`; + return numeroComplet === existingLocationNumeroComplet; + }); + if (existingLocation) { + existingLocation.toponyme = toponyme; + if ((existingLocation as Numero).voieId) { + const voie = voies.find( + (voie) => voie.id === (existingLocation as Numero).voieId + ); + (existingLocation as Numero).voie = voie; + } + } + } + } + + return existingLocation; +} + +export const detectChanges = ( + signalement: Signalement, + existingLocation: Numero +) => { + const { numero, suffixe, positions, parcelles, nomVoie, nomComplement } = + signalement.changesRequested as NumeroChangesRequestedDTO; + + const numeroComplet = `${numero}${suffixe ? suffixe : ""}`; + + const { + numeroComplet: existingNumeroComplet, + positions: existingPositions, + parcelles: existingParcelles, + voie: existingVoie, + toponyme: existingToponyme, + } = existingLocation; + + return { + voie: nomVoie !== existingVoie?.nom, + numero: numeroComplet !== existingNumeroComplet, + positions: + JSON.stringify(positions.map(({ point, type }) => ({ point, type }))) !== + JSON.stringify( + existingPositions.map(({ point, type }) => ({ point, type })) + ), + parcelles: JSON.stringify(parcelles) !== JSON.stringify(existingParcelles), + complement: nomComplement && nomComplement !== existingToponyme?.nom, + }; +}; diff --git a/package.json b/package.json index b3af652bb..4e96e24e8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "date-fns": "2.29.3", "evergreen-ui": "^7.1.9", "express": "4.18.2", + "fast-diff": "^1.3.0", "fs-extra": "^11.1.1", "fuse.js": "^6.6.2", "get-stream": "^6.0.1", @@ -79,4 +80,4 @@ "engines": { "node": "22" } -} \ No newline at end of file +} diff --git a/pages/_app.tsx b/pages/_app.tsx index 224fa9022..679027988 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -127,6 +127,24 @@ function App(props: AppProps) { .main-table-cell:hover { background-color: #e4e7eb; } + + .glass-pane { + /* From https://css.glass */ + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + @keyframes delay-bar { + 0% { + width: 100%; + } + 100% { + width: 0; + } + } `} ); diff --git a/pages/bal/[balId]/signalements/[idSignalement].tsx b/pages/bal/[balId]/signalements/[idSignalement].tsx index 9fe381a5a..59f028c2c 100644 --- a/pages/bal/[balId]/signalements/[idSignalement].tsx +++ b/pages/bal/[balId]/signalements/[idSignalement].tsx @@ -1,135 +1,196 @@ -import React, { useState } from "react"; -import { Heading, Pane, Paragraph, Tab, Tablist } from "evergreen-ui"; -import { uniqueId } from "lodash"; +import React, { useCallback, useContext, useEffect } from "react"; +import { Button, Link, Pane, Paragraph, Text } from "evergreen-ui"; +import NextLink from "next/link"; import { BaseEditorProps, getBaseEditorProps } from "@/layouts/editor"; -import ProtectedPage from "@/layouts/protected-page"; -import { - Numero, - Toponyme, - ToponymesService, - Voie, - VoiesService, -} from "@/lib/openapi-api-bal"; +import { Numero, Toponyme, Voie } from "@/lib/openapi-api-bal"; import { - ExistingLocation, + ExistingVoie, + NumeroChangesRequestedDTO, Signalement, - DefaultService as SignalementService, + SignalementsService, } from "@/lib/openapi-signalement"; +import { SignalementsService as SignalementsServiceBal } from "@/lib/openapi-api-bal"; import { useRouter } from "next/router"; -import SignalementUpdateNumero from "@/components/signalement/numero/signalement-update-numero"; -import SignalementViewer from "@/components/signalement/signalement-viewer"; -import SignalementDeleteNumero from "@/components/signalement/numero/signalement-delete-numero"; -import SignalementCreateNumero from "@/components/signalement/numero/signalement-create-numero"; -import SignalementUpdateVoie from "@/components/signalement/voie/signalement-update-voie"; -import SignalementUpdateToponyme from "@/components/signalement/toponyme/signalement-update-toponyme"; +import LayoutContext from "@/contexts/layout"; +import { + getExistingLocation, + getSignalementLabel, +} from "@/lib/utils/signalement"; +import { CommuneType } from "@/types/commune"; +import { ObjectId } from "bson"; +import MapContext from "@/contexts/map"; +import BalDataContext from "@/contexts/bal-data"; +import ProtectedPage from "@/layouts/protected-page"; +import SignalementForm from "@/components/signalement/signalement-form/signalement-form"; +import { SignalementViewer } from "@/components/signalement/signalement-viewer/signalement-viewer"; interface SignalementPageProps extends BaseEditorProps { signalement: Signalement; existingLocation: Voie | Toponyme | Numero | null; + requestedToponyme?: Toponyme; + commune: CommuneType; } function SignalementPage({ signalement, existingLocation, + requestedToponyme, commune, + baseLocale, }: SignalementPageProps) { - const [activeTab, setActiveTab] = useState(1); const router = useRouter(); + const { toaster, setBreadcrumbs } = useContext(LayoutContext); + const { showTilesLayers, setShowToponymes } = useContext(MapContext); + const { refreshBALSync } = useContext(BalDataContext); + + useEffect(() => { + showTilesLayers(false); + setShowToponymes(false); + setBreadcrumbs( + <> + + {baseLocale.nom || commune.nom} + + + {" > "} + + Signalements + + {" > "} + {getSignalementLabel(signalement)} + + ); + + return () => { + showTilesLayers(true); + setShowToponymes(true); + setBreadcrumbs(null); + }; + }, [ + showTilesLayers, + setShowToponymes, + setBreadcrumbs, + baseLocale, + signalement, + commune, + ]); + + // Mark the signalement as expired if the location is not found + // and the signalement is still pending + useEffect(() => { + const markSignalementAsExpired = async () => { + await SignalementsServiceBal.updateSignalements(baseLocale.id, { + ids: [signalement.id], + status: Signalement.status.EXPIRED, + }); + }; + + if ( + (existingLocation === null || requestedToponyme === null) && + signalement.status === Signalement.status.PENDING + ) { + markSignalementAsExpired(); + } + }, [existingLocation, signalement, baseLocale, requestedToponyme]); - const handleClose = () => { + const handleClose = useCallback(() => { router.push(`/bal/${router.query.balId}/signalements`); - }; + }, [router]); + + const getNextSignalement = useCallback(async () => { + const nextPaginatedSignalement = await SignalementsService.getSignalements( + undefined, + 1, + [Signalement.status.PENDING], + undefined, + undefined, + [commune.code] + ); - const handleSignalementProcessed = async () => { - await SignalementService.updateSignalement({ - id: signalement._id as string, - }); - handleClose(); - }; + if (nextPaginatedSignalement.data.length > 0) { + return (nextPaginatedSignalement.data as unknown as Signalement[])[0]; + } + }, [commune.code]); + + const handleSubmit = useCallback( + async (status: Signalement.status) => { + const _updateSignalement = toaster( + async () => { + await SignalementsServiceBal.updateSignalements(baseLocale.id, { + ids: [signalement.id], + status, + }); + await refreshBALSync(); + }, + status === Signalement.status.PROCESSED + ? "Le signalement a bien été pris en compte" + : "Le signalement a bien été ignoré", + "Une erreur est survenue" + ); + + await _updateSignalement(); + + const nextSignalement = await getNextSignalement(); + + if (nextSignalement) { + router.push( + `/bal/${router.query.balId}/signalements/${nextSignalement.id}` + ); + } else { + handleClose(); + } + }, + [ + signalement, + toaster, + handleClose, + getNextSignalement, + router, + baseLocale, + refreshBALSync, + ] + ); return ( - {existingLocation ? ( - <> - - {["Infos", "Editeur"].map((tab, index) => ( - setActiveTab(index)} - > - {tab} - - ))} - - - {activeTab === 0 && ( - - )} - {activeTab === 1 && ( - <> - {signalement.type === Signalement.type.LOCATION_TO_CREATE && ( - - )} - {signalement.type === Signalement.type.LOCATION_TO_UPDATE && - (signalement.existingLocation.type === - ExistingLocation.type.NUMERO ? ( - - ) : signalement.existingLocation.type === - ExistingLocation.type.VOIE ? ( - - ) : ( - - ))} - {signalement.type === Signalement.type.LOCATION_TO_DELETE && ( - - )} - - )} - - + {existingLocation && requestedToponyme !== null ? ( + + + + ) : signalement.status === Signalement.status.IGNORED || + signalement.status === Signalement.status.PROCESSED ? ( + + router.push(`/bal/${router.query.balId}/signalements?tab=archived`) + } + /> ) : ( - Erreur : Impossible de trouver la localisation du signalement. + + Il a été marqué comme expiré et n'apparaîtra plus dans la liste + des signalements. + + )} @@ -143,21 +204,46 @@ export async function getServerSideProps({ params }) { const { baseLocale, commune, voies, toponymes }: BaseEditorProps = await getBaseEditorProps(balId); - const signalements = await SignalementService.getSignalementsByCodeCommune( - baseLocale.commune + const signalement = await SignalementsService.getSignalementById( + params.idSignalement ); - const signalement = signalements.find( - (signalement) => signalement._id === params.idSignalement - ); + if ( + signalement.status === Signalement.status.PROCESSED || + signalement.status === Signalement.status.IGNORED + ) { + return { + props: { + baseLocale, + commune, + voies, + toponymes, + signalement, + existingLocation: null, + }, + }; + } + + if ((signalement.changesRequested as NumeroChangesRequestedDTO).positions) { + (signalement.changesRequested as NumeroChangesRequestedDTO).positions = ( + signalement.changesRequested as NumeroChangesRequestedDTO + ).positions.map((p) => ({ + ...p, + id: new ObjectId().toHexString(), + })); + } - if (signalement.changesRequested.positions) { - signalement.changesRequested.positions = - signalement.changesRequested.positions.map((p) => ({ - ...p, - _id: uniqueId(), - source: "signalement", - })); + let requestedToponyme; + if ( + (signalement.changesRequested as NumeroChangesRequestedDTO).nomComplement + ) { + requestedToponyme = + toponymes.find( + (toponyme) => + toponyme.nom === + (signalement.changesRequested as NumeroChangesRequestedDTO) + .nomComplement + ) || null; } let existingLocation = null; @@ -165,61 +251,26 @@ export async function getServerSideProps({ params }) { signalement.type === Signalement.type.LOCATION_TO_UPDATE || signalement.type === Signalement.type.LOCATION_TO_DELETE ) { - if (signalement.existingLocation.type === ExistingLocation.type.VOIE) { - existingLocation = voies.find( - (voie) => voie.nom === signalement.existingLocation.nom + try { + existingLocation = await getExistingLocation( + signalement, + voies, + toponymes ); - } else if ( - signalement.existingLocation.type === ExistingLocation.type.TOPONYME - ) { - existingLocation = toponymes.find( - (toponyme) => toponyme.nom === signalement.existingLocation.nom - ); - } else if ( - signalement.existingLocation.type === ExistingLocation.type.NUMERO - ) { - if ( - signalement.existingLocation.toponyme.type === - ExistingLocation.type.VOIE - ) { - const voie = voies.find( - (voie) => voie.nom === signalement.existingLocation.toponyme.nom - ); - const numeros = await VoiesService.findVoieNumeros(voie.id); - existingLocation = numeros.find(({ numeroComplet }) => { - const existingLocationNumeroComplet = signalement.existingLocation - .suffixe - ? `${signalement.existingLocation.numero}${signalement.existingLocation.suffixe}` - : `${signalement.existingLocation.numero}`; - return numeroComplet === existingLocationNumeroComplet; - }); - if (existingLocation) { - existingLocation.voie = voie; - } - } else { - const toponyme = toponymes.find( - (toponyme) => - toponyme.nom === signalement.existingLocation.toponyme.nom - ); - const numeros = await ToponymesService.findToponymeNumeros( - toponyme.id - ); - existingLocation = numeros.find(({ numeroComplet }) => { - const existingLocationNumeroComplet = signalement.existingLocation - .suffixe - ? `${signalement.existingLocation.numero}${signalement.existingLocation.suffixe}` - : `${signalement.existingLocation.numero}`; - return numeroComplet === existingLocationNumeroComplet; - }); - if (existingLocation) { - existingLocation.toponyme = toponyme; - } - } + } catch (err) { + console.error(err); + existingLocation = null; } } else if (signalement.type === Signalement.type.LOCATION_TO_CREATE) { - existingLocation = voies.find( - (voie) => voie.nom === signalement.existingLocation.nom - ); + existingLocation = voies.find((voie) => { + if ((signalement.existingLocation as ExistingVoie).banId) { + return ( + voie.banId === (signalement.existingLocation as ExistingVoie).banId + ); + } + + return voie.nom === (signalement.existingLocation as ExistingVoie).nom; + }); } if (!existingLocation) { @@ -234,6 +285,7 @@ export async function getServerSideProps({ params }) { toponymes, signalement, existingLocation, + ...(requestedToponyme !== undefined ? { requestedToponyme } : {}), }, }; } catch (err) { diff --git a/pages/bal/[balId]/signalements/index.tsx b/pages/bal/[balId]/signalements/index.tsx index 0dceb1f53..ee8c852a3 100644 --- a/pages/bal/[balId]/signalements/index.tsx +++ b/pages/bal/[balId]/signalements/index.tsx @@ -1,29 +1,64 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { Button, Dialog, Heading, Pane, Paragraph, + Tab, + Tablist, TrashIcon, + Text, } from "evergreen-ui"; import { BaseEditorProps, getBaseEditorProps } from "@/layouts/editor"; import SignalementList from "@/components/signalement/signalement-list"; import { useRouter } from "next/router"; import ProtectedPage from "@/layouts/protected-page"; +import { SignalementsService as SignalementsServiceBal } from "@/lib/openapi-api-bal"; import { ExistingNumero, + NumeroChangesRequestedDTO, Signalement, - DefaultService as SignalementService, + SignalementsService, } from "@/lib/openapi-signalement"; import MarkersContext from "@/contexts/markers"; import { getSignalementLabel } from "@/lib/utils/signalement"; import LayoutContext from "@/contexts/layout"; +import SignalementTypeBadge, { + signalementTypeMap, +} from "@/components/signalement/signalement-type-badge"; +import useFuse from "@/hooks/fuse"; +import MapContext from "@/contexts/map"; +import bbox from "@turf/bbox"; -function SignalementsPage({ baseLocale, signalements: initialSignalements }) { - const [signalements, setSignalements] = - useState(initialSignalements); +const fuseOptions = { + keys: ["label"], +}; + +interface SignalementsPageProps extends BaseEditorProps { + paginatedSignalements: { data: Signalement[] }; +} + +const tabs = [ + { label: "En cours", key: "pending" }, + { label: "Archivés", key: "archived" }, +]; + +function SignalementsPage({ + baseLocale, + commune, + paginatedSignalements: initialSignalements, +}: SignalementsPageProps) { + const [signalements, setSignalements] = useState( + initialSignalements.data + ); const [selectedSignalements, setSelectedSignalements] = useState( [] ); @@ -31,34 +66,125 @@ function SignalementsPage({ baseLocale, signalements: initialSignalements }) { const router = useRouter(); const { addMarker, disableMarkers } = useContext(MarkersContext); const { toaster } = useContext(LayoutContext); + const { showTilesLayers, setShowToponymes, setViewport } = + useContext(MapContext); + const [activeTabIndex, setActiveTabIndex] = useState( + router.query.tab === "archived" ? 1 : 0 + ); + const [filters, setFilters] = useState<{ type: Signalement.type[] }>({ + type: [], + }); + + const fetchSignalements = useCallback( + async ( + status: Signalement.status[] = [Signalement.status.PENDING], + types?: Signalement.type[] + ) => { + const paginatedSignalements = await SignalementsService.getSignalements( + 100, + undefined, + status, + types, + undefined, + [baseLocale.commune] + ); + setSignalements(paginatedSignalements.data as unknown as Signalement[]); + }, + [baseLocale.commune] + ); + // Set viewport to commune useEffect(() => { - const markerPositions = signalements - .reduce((acc, cur) => { - let position; - if (cur.type === Signalement.type.LOCATION_TO_CREATE) { - position = cur.changesRequested?.positions[0]?.point; - } else { - position = (cur.existingLocation as ExistingNumero).position?.point; - } - - return [ - ...acc, - { - signalementId: cur._id, - label: getSignalementLabel(cur, { withoutDate: true }), - position: position, - }, - ]; - }, []) + const communeBbox: number[] = bbox(commune.contour); + if (communeBbox) { + setViewport({ + latitude: (communeBbox[1] + communeBbox[3]) / 2, + longitude: (communeBbox[0] + communeBbox[2]) / 2, + zoom: 12, + }); + } + }, [commune, setViewport]); + + useEffect(() => { + showTilesLayers(false); + setShowToponymes(false); + + return () => { + showTilesLayers(true); + setShowToponymes(true); + }; + }, [showTilesLayers, setShowToponymes]); + + useEffect(() => { + const status = + activeTabIndex === 0 + ? [Signalement.status.PENDING] + : [Signalement.status.IGNORED, Signalement.status.PROCESSED]; + + setSelectedSignalements([]); + fetchSignalements(status, filters.type); + }, [filters, activeTabIndex, fetchSignalements]); + + const signalementsWithLabel = useMemo( + () => signalements.map((s) => ({ ...s, label: getSignalementLabel(s) })), + [signalements] + ); + + const [signalementsList, setSignalementsList] = useFuse( + signalementsWithLabel, + 200, + fuseOptions + ); + + const handleSelectSignalement = useCallback( + (id) => { + router.push(`/bal/${router.query.balId}/signalements/${id}`); + }, + [router] + ); + + useEffect(() => { + const markerPositions = signalementsList + .reduce( + ( + acc, + { id, label, type, changesRequested, existingLocation, status } + ) => { + let position; + if (type === Signalement.type.LOCATION_TO_CREATE) { + position = (changesRequested as NumeroChangesRequestedDTO) + ?.positions[0]?.point; + } else { + position = (existingLocation as ExistingNumero).position?.point; + } + + return [ + ...acc, + { + signalementId: id, + status, + label: ( + + + {label} + + ), + type: type, + position: position, + }, + ]; + }, + [] + ) .filter((signalement) => signalement.position) - .map(({ position, signalementId, label }) => { + .map(({ position, signalementId, label, type }) => { return { + id: signalementId, isMapMarker: true, - label, + tooltip: label, longitude: position.coordinates[0], latitude: position.coordinates[1], - color: "warning", + color: signalementTypeMap[type].color, onClick: () => { handleSelectSignalement(signalementId); }, @@ -72,61 +198,59 @@ function SignalementsPage({ baseLocale, signalements: initialSignalements }) { return () => { disableMarkers(); }; - }, [signalements]); - - const refreshSignalements = async () => { - const signalements = await SignalementService.getSignalementsByCodeCommune( - baseLocale.commune - ); - setSignalements(signalements); - }; - - const handleSelectSignalement = (id) => { - router.push(`/bal/${router.query.balId}/signalements/${id}`); - }; + }, [signalementsList, handleSelectSignalement]); const handleIgnoreSignalement = async (id) => { - const updateSignalement = toaster( - () => SignalementService.updateSignalement({ id }), + const _updateSignalement = toaster( + () => + SignalementsServiceBal.updateSignalements(baseLocale.id, { + ids: [id], + status: Signalement.status.IGNORED, + }), "Le signalement a bien été ignoré", "Une erreur est survenue" ); - await updateSignalement(); - await refreshSignalements(); + await _updateSignalement(); + await fetchSignalements(); }; const handleIgnoreSignalements = async () => { const massUpdateSignalements = toaster( async () => { - for (const id of selectedSignalements) { - await SignalementService.updateSignalement({ id }); - } + await SignalementsServiceBal.updateSignalements(baseLocale.id, { + ids: selectedSignalements, + status: Signalement.status.IGNORED, + }); }, "Les signalements ont bien été ignorés", "Une erreur est survenue" ); await massUpdateSignalements(); - await refreshSignalements(); + await fetchSignalements(); }; const handleToggleSelect = (ids: string[]) => { - if (ids.length === signalements.length) { - setSelectedSignalements(ids); - } else if (ids.length === 0) { - setSelectedSignalements([]); - } else { - for (const id of ids) { - if (!selectedSignalements.includes(id)) { - setSelectedSignalements([...selectedSignalements, id]); - } else { - setSelectedSignalements(selectedSignalements.filter((s) => s !== id)); - } + for (const id of ids) { + if (!selectedSignalements.includes(id)) { + setSelectedSignalements([...selectedSignalements, id]); + } else { + setSelectedSignalements(selectedSignalements.filter((s) => s !== id)); } } }; + const handleSelectTab = (index: number) => { + setActiveTabIndex(index); + router.replace({ + query: { + ...router.query, + tab: tabs[index].key, + }, + }); + }; + return ( + + {tabs.map(({ label, key }, index) => ( + handleSelectTab(index)} + > + {label} + + ))} + + )} @@ -218,8 +359,13 @@ export async function getServerSideProps({ params }) { const { baseLocale, commune, voies, toponymes }: BaseEditorProps = await getBaseEditorProps(balId); - const signalements = await SignalementService.getSignalementsByCodeCommune( - baseLocale.commune + const paginatedSignalements = await SignalementsService.getSignalements( + 100, + undefined, + [Signalement.status.PENDING], + undefined, + undefined, + [baseLocale.commune] ); return { @@ -228,7 +374,7 @@ export async function getServerSideProps({ params }) { commune, voies, toponymes, - signalements, + paginatedSignalements, }, }; } catch { diff --git a/pages/bal/[balId]/toponymes/[idToponyme].tsx b/pages/bal/[balId]/toponymes/[idToponyme].tsx index ae19cb163..34d6d593c 100644 --- a/pages/bal/[balId]/toponymes/[idToponyme].tsx +++ b/pages/bal/[balId]/toponymes/[idToponyme].tsx @@ -38,6 +38,10 @@ interface ToponymePageProps { commune: CommuneType; } +const fuseOptions = { + keys: ["numero"], +}; + function ToponymePage({ baseLocale, commune }: ToponymePageProps) { const { isFormOpen, handleEditing, editedNumero, reset } = useFormState(); @@ -52,9 +56,7 @@ function ToponymePage({ baseLocale, commune }: ToponymePageProps) { useContext(BalDataContext); useHelp(2); - const [filtered, setFilter] = useFuse(numeros, 200, { - keys: ["numero"], - }); + const [filtered, setFilter] = useFuse(numeros, 200, fuseOptions); const onAdd = async (numeros: string[]) => { setIsLoading(true); diff --git a/pages/bal/[balId]/voies/[idVoie].tsx b/pages/bal/[balId]/voies/[idVoie].tsx index 1cb796901..c2f72e61e 100644 --- a/pages/bal/[balId]/voies/[idVoie].tsx +++ b/pages/bal/[balId]/voies/[idVoie].tsx @@ -15,7 +15,6 @@ import { BaseEditorProps, getBaseEditorProps } from "@/layouts/editor"; import { ExtendedVoieDTO, Numero, - OpenAPI, VoieMetas, VoiesService, } from "@/lib/openapi-api-bal"; diff --git a/yarn.lock b/yarn.lock index 7196f1631..b36dc23d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2706,6 +2706,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-fifo@^1.1.0, fast-fifo@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" From 340ae21cc12ae9b64e885b9ded2b6eb1182194d2 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 6 Dec 2024 17:46:18 +0100 Subject: [PATCH 02/10] fix: no white list if the env var is undefined --- components/bal/commune-tab.tsx | 9 +++-- components/bal/signalement-infos.tsx | 51 +++++++++++++----------- components/base-locale-card/index.tsx | 10 +---- components/sidebar/tabs.tsx | 4 +- contexts/signalement.tsx | 56 ++++++++++++++------------- lib/utils/signalement.ts | 17 ++++++++ pages/bal/[balId]/index.tsx | 1 + 7 files changed, 87 insertions(+), 61 deletions(-) diff --git a/components/bal/commune-tab.tsx b/components/bal/commune-tab.tsx index ca49097d1..95ccd37ee 100644 --- a/components/bal/commune-tab.tsx +++ b/components/bal/commune-tab.tsx @@ -20,13 +20,16 @@ interface CommuneTabProps { function CommuneTab({ commune, openRecoveryDialog }: CommuneTabProps) { const { baseLocale } = useContext(BalDataContext); const { token } = useContext(TokenContext); - const { signalements } = useContext(SignalementContext); + const { signalementCounts } = useContext(SignalementContext); return ( {!token && } - {signalements.length > 0 && ( - + {(signalementCounts.pending > 0 || signalementCounts.archived > 0) && ( + )} {token && baseLocale.status !== BaseLocale.status.DEMO && ( diff --git a/components/bal/signalement-infos.tsx b/components/bal/signalement-infos.tsx index cb611e47d..3e84ca50e 100644 --- a/components/bal/signalement-infos.tsx +++ b/components/bal/signalement-infos.tsx @@ -1,19 +1,31 @@ import React from "react"; import { Pane, Heading, Button, Alert } from "evergreen-ui"; -import { Signalement } from "@/lib/openapi-signalement"; import { useRouter } from "next/navigation"; +import { SignalementCounts } from "@/contexts/signalement"; interface SignalementInfosProps { balId: string; - signalements: Signalement[]; + signalementCounts: SignalementCounts; } -function SignalementInfos({ balId, signalements }: SignalementInfosProps) { +function SignalementInfos({ balId, signalementCounts }: SignalementInfosProps) { const router = useRouter(); const onClick = () => { router.push(`/bal/${balId}/signalements`); }; + const signalementBtn = ( + + ); + return ( Signalements - - Vous avez reçu {signalements.length}{" "} - {signalements.length > 1 ? "propositions" : "proposition"}. - - } - > - - + {signalementBtn} + + ) : ( + {signalementBtn} + )} ); } diff --git a/components/base-locale-card/index.tsx b/components/base-locale-card/index.tsx index 4d8d81cdc..d1565865a 100644 --- a/components/base-locale-card/index.tsx +++ b/components/base-locale-card/index.tsx @@ -24,6 +24,7 @@ import { } from "@/lib/openapi-api-bal"; import { CommuneApiGeoType } from "@/lib/geo-api/type"; import { Signalement, SignalementsService } from "@/lib/openapi-signalement"; +import { canFetchSignalements } from "@/lib/utils/signalement"; const ADRESSE_URL = process.env.NEXT_PUBLIC_ADRESSE_URL || "https://adresse.data.gouv.fr"; @@ -121,14 +122,7 @@ function BaseLocaleCard({ if (!baseLocale.token) { void fetchHabilitationIsValid(); } else { - const signalementWhiteList = - process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST?.split(",") || - []; - if ( - baseLocale.status === BaseLocale.status.PUBLISHED && - process.env.NEXT_PUBLIC_API_SIGNALEMENT && - signalementWhiteList.includes(baseLocale.commune) - ) { + if (canFetchSignalements(baseLocale, baseLocale.token)) { void fetchPendingSignalementsCount(); } void fetchHabilitation(); diff --git a/components/sidebar/tabs.tsx b/components/sidebar/tabs.tsx index 291627154..38d4a1550 100644 --- a/components/sidebar/tabs.tsx +++ b/components/sidebar/tabs.tsx @@ -18,7 +18,7 @@ interface TabsSideBarProps { function TabsSideBar({ selectedTab, balId }: TabsSideBarProps) { const { isMobile } = useContext(LayoutContext); - const { signalements } = useContext(SignalementContext); + const { signalementCounts } = useContext(SignalementContext); return ( <> @@ -34,7 +34,7 @@ function TabsSideBar({ selectedTab, balId }: TabsSideBarProps) { { key: TabsEnum.COMMUNE, label: "Commune", - notif: signalements.length, + notif: signalementCounts.pending, href: `/bal/${balId}?selectedTab=${TabsEnum.COMMUNE}`, }, { diff --git a/contexts/signalement.tsx b/contexts/signalement.tsx index bbca8f617..7f15e4e12 100644 --- a/contexts/signalement.tsx +++ b/contexts/signalement.tsx @@ -6,14 +6,15 @@ import React, { useMemo, } from "react"; -import { BaseLocale } from "@/lib/openapi-api-bal"; import { Signalement, SignalementsService } from "@/lib/openapi-signalement"; import { ChildrenProps } from "@/types/context"; import BalDataContext from "./bal-data"; import TokenContext from "./token"; +import { canFetchSignalements } from "@/lib/utils/signalement"; +export type SignalementCounts = { pending: number; archived: number }; interface SignalementContextType { - signalements: Signalement[]; + signalementCounts: SignalementCounts; } const SignalementContext = React.createContext( @@ -21,47 +22,50 @@ const SignalementContext = React.createContext( ); export function SignalementContextProvider(props: ChildrenProps) { - const [signalements, setSignalements] = useState([]); + const [signalementCounts, setSignalementCounts] = useState( + { + pending: 0, + archived: 0, + } + ); const { baseLocale } = useContext(BalDataContext); const { token } = useContext(TokenContext); - const fetchSignalements = useCallback(async () => { - const paginatedSignalements = await SignalementsService.getSignalements( - undefined, + const fetchSignalementCount = useCallback(async () => { + const pendingCount = await SignalementsService.getSignalements( + 1, undefined, [Signalement.status.PENDING], undefined, undefined, [baseLocale.commune] ); - setSignalements(paginatedSignalements.data); + const archivedCount = await SignalementsService.getSignalements( + 1, + undefined, + [Signalement.status.PROCESSED, Signalement.status.EXPIRED], + undefined, + undefined, + [baseLocale.commune] + ); + + setSignalementCounts({ + pending: pendingCount.total, + archived: archivedCount.total, + }); }, [baseLocale.commune]); useEffect(() => { - const signalementWhiteList = - process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST?.split(",") || []; - - const isSignalementEnabled = - Boolean(process.env.NEXT_PUBLIC_API_SIGNALEMENT) && - signalementWhiteList.includes(baseLocale.commune); - - if (!isSignalementEnabled) { - return; - } - - const canGetSignalement = - baseLocale.status === BaseLocale.status.PUBLISHED && Boolean(token); - - if (canGetSignalement) { - fetchSignalements(); + if (canFetchSignalements(baseLocale, token)) { + fetchSignalementCount(); } - }, [baseLocale, token, fetchSignalements]); + }, [baseLocale, token, fetchSignalementCount]); const value = useMemo( () => ({ - signalements, + signalementCounts, }), - [signalements] + [signalementCounts] ); return ; diff --git a/lib/utils/signalement.ts b/lib/utils/signalement.ts index 5dac5ca3d..a17367e75 100644 --- a/lib/utils/signalement.ts +++ b/lib/utils/signalement.ts @@ -1,4 +1,5 @@ import { + BaseLocale, Numero, Toponyme, ToponymesService, @@ -216,3 +217,19 @@ export const detectChanges = ( complement: nomComplement && nomComplement !== existingToponyme?.nom, }; }; + +export const canFetchSignalements = (baseLocale: BaseLocale, token: string) => { + const noSignalementWhiteList = + process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST === undefined; + + const signalementWhiteList = + process.env.NEXT_PUBLIC_SIGNALEMENT_COMMUNES_WHITE_LIST?.split(",") || []; + + return ( + baseLocale.status === BaseLocale.status.PUBLISHED && + Boolean(token) && + process.env.NEXT_PUBLIC_API_SIGNALEMENT !== undefined && + (noSignalementWhiteList || + signalementWhiteList.includes(baseLocale.commune)) + ); +}; diff --git a/pages/bal/[balId]/index.tsx b/pages/bal/[balId]/index.tsx index e00b44bb8..dbdab0a0b 100644 --- a/pages/bal/[balId]/index.tsx +++ b/pages/bal/[balId]/index.tsx @@ -60,6 +60,7 @@ function BaseLocalePage({ commune }: BaseLocalePageProps) { const { handleShowHabilitationProcess } = usePublishProcess(commune); const { refreshBALSync, reloadVoies, reloadToponymes, reloadParcelles } = useContext(BalDataContext); + const router = useRouter(); const selectedTab: TabsEnum = (router.query.selectedTab as TabsEnum) || TabsEnum.VOIES; From 064cdf5361c3183c5bbb8a5efe506ba5160078cb Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Tue, 10 Dec 2024 10:48:43 +0100 Subject: [PATCH 03/10] fix: request signalements context ignored instead of expired --- contexts/signalement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contexts/signalement.tsx b/contexts/signalement.tsx index 7f15e4e12..47637aa5a 100644 --- a/contexts/signalement.tsx +++ b/contexts/signalement.tsx @@ -43,7 +43,7 @@ export function SignalementContextProvider(props: ChildrenProps) { const archivedCount = await SignalementsService.getSignalements( 1, undefined, - [Signalement.status.PROCESSED, Signalement.status.EXPIRED], + [Signalement.status.PROCESSED, Signalement.status.IGNORED], undefined, undefined, [baseLocale.commune] From 08173274f001831a6814c09d95e02e6d78ba5d6b Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 12 Dec 2024 18:28:30 +0100 Subject: [PATCH 04/10] feat: retour atelier signalement Thumeries --- components/bal/commune-tab.tsx | 10 +- components/bal/signalement-infos.tsx | 6 +- components/sidebar/tabs.tsx | 6 +- .../hooks/useSignalementMapDiffCreation.ts | 78 ++++++++++ .../hooks/useSignalementMapDiffDeletion.ts | 75 ++++++++++ .../hooks/useSignalementMapDiffUpdate.ts | 51 +++---- .../signalement-diff/accordion-card.tsx | 33 ++--- .../signalement-numero-diff-card.tsx | 9 +- .../signalement-parcelle-diff.tsx | 3 +- .../signalement-toponyme-diff-card.tsx | 9 +- .../signalement-diff/text-diff.tsx | 10 ++ .../numero/signalement-create-numero.tsx | 58 +------- .../numero/signalement-delete-numero.tsx | 55 +------ .../numero/signalement-update-numero.tsx | 16 +- .../signalement-form-buttons.tsx | 14 +- .../signalement-form/signalement-form.tsx | 39 ++++- .../toponyme/signalement-update-toponyme.tsx | 10 +- components/signalement/signalement-header.tsx | 75 +++++++--- .../signalement/signalement-joyride.tsx | 136 +++++++++++++++++ components/signalement/signalement-list.tsx | 3 +- .../signalement-viewer-create-numero.tsx | 55 +------ .../signalement-viewer-delete-numero.tsx | 53 +------ .../signalement-viewer-update-numero.tsx | 10 +- .../signalement-viewer/signalement-viewer.tsx | 29 +++- .../signalement-viewer-update-toponyme.tsx | 10 +- contexts/local-storage.tsx | 8 + contexts/map.tsx | 2 +- contexts/parcelles.tsx | 43 ++---- contexts/signalement.tsx | 119 +++++++++++---- contexts/token.tsx | 2 +- layouts/protected-page.tsx | 18 ++- .../services/SignalementsService.ts | 21 +++ lib/openapi-signalement/models/Author.ts | 2 + lib/openapi-signalement/models/AuthorInput.ts | 2 + .../services/SignalementsService.ts | 1 + package.json | 1 + .../[balId]/signalements/[idSignalement].tsx | 46 ++---- pages/bal/[balId]/signalements/index.tsx | 138 +++++++++--------- yarn.lock | 99 +++++++++++++ 39 files changed, 854 insertions(+), 501 deletions(-) create mode 100644 components/signalement/hooks/useSignalementMapDiffCreation.ts create mode 100644 components/signalement/hooks/useSignalementMapDiffDeletion.ts rename hooks/useSignalementMapDiff.ts => components/signalement/hooks/useSignalementMapDiffUpdate.ts (86%) create mode 100644 components/signalement/signalement-joyride.tsx diff --git a/components/bal/commune-tab.tsx b/components/bal/commune-tab.tsx index 95ccd37ee..debfb1bcf 100644 --- a/components/bal/commune-tab.tsx +++ b/components/bal/commune-tab.tsx @@ -20,15 +20,19 @@ interface CommuneTabProps { function CommuneTab({ commune, openRecoveryDialog }: CommuneTabProps) { const { baseLocale } = useContext(BalDataContext); const { token } = useContext(TokenContext); - const { signalementCounts } = useContext(SignalementContext); + const { pendingSignalementsCount, archivedSignalementsCount } = + useContext(SignalementContext); return ( {!token && } - {(signalementCounts.pending > 0 || signalementCounts.archived > 0) && ( + {(pendingSignalementsCount > 0 || archivedSignalementsCount > 0) && ( )} {token && baseLocale.status !== BaseLocale.status.DEMO && ( diff --git a/components/bal/signalement-infos.tsx b/components/bal/signalement-infos.tsx index 3e84ca50e..36831280a 100644 --- a/components/bal/signalement-infos.tsx +++ b/components/bal/signalement-infos.tsx @@ -1,11 +1,13 @@ import React from "react"; import { Pane, Heading, Button, Alert } from "evergreen-ui"; import { useRouter } from "next/navigation"; -import { SignalementCounts } from "@/contexts/signalement"; interface SignalementInfosProps { balId: string; - signalementCounts: SignalementCounts; + signalementCounts: { + pending: number; + archived: number; + }; } function SignalementInfos({ balId, signalementCounts }: SignalementInfosProps) { diff --git a/components/sidebar/tabs.tsx b/components/sidebar/tabs.tsx index 38d4a1550..3c6280bcf 100644 --- a/components/sidebar/tabs.tsx +++ b/components/sidebar/tabs.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo } from "react"; +import React, { useContext } from "react"; import { Pane, Tablist, Tab, Tooltip } from "evergreen-ui"; import Link from "next/link"; @@ -18,7 +18,7 @@ interface TabsSideBarProps { function TabsSideBar({ selectedTab, balId }: TabsSideBarProps) { const { isMobile } = useContext(LayoutContext); - const { signalementCounts } = useContext(SignalementContext); + const { pendingSignalementsCount } = useContext(SignalementContext); return ( <> @@ -34,7 +34,7 @@ function TabsSideBar({ selectedTab, balId }: TabsSideBarProps) { { key: TabsEnum.COMMUNE, label: "Commune", - notif: signalementCounts.pending, + notif: pendingSignalementsCount, href: `/bal/${balId}?selectedTab=${TabsEnum.COMMUNE}`, }, { diff --git a/components/signalement/hooks/useSignalementMapDiffCreation.ts b/components/signalement/hooks/useSignalementMapDiffCreation.ts new file mode 100644 index 000000000..61a783a25 --- /dev/null +++ b/components/signalement/hooks/useSignalementMapDiffCreation.ts @@ -0,0 +1,78 @@ +import MapContext from "@/contexts/map"; +import MarkersContext from "@/contexts/markers"; +import ParcellesContext from "@/contexts/parcelles"; +import { useContext, useEffect } from "react"; +import { + NumeroChangesRequestedDTO, + Position as PositionSignalement, +} from "@/lib/openapi-signalement"; +import { getPositionName } from "@/lib/positions-types-list"; + +export function useSignalementMapDiffCreation( + changesRequested: NumeroChangesRequestedDTO +) { + const { parcelles, positions } = changesRequested; + + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed, map } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (!map) { + return; + } + + const initCb = () => { + setHighlightedParcelles(parcelles); + }; + map.once("moveend", initCb); + + return () => { + map.off("moveend", initCb); + }; + }, [map, setHighlightedParcelles, parcelles]); + + useEffect(() => { + if (positions?.length > 0) { + positions.forEach((position: PositionSignalement & { id: string }) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: "gray", + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + label: getPositionName(position.type), + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positions, addMarker, disableMarkers]); +} diff --git a/components/signalement/hooks/useSignalementMapDiffDeletion.ts b/components/signalement/hooks/useSignalementMapDiffDeletion.ts new file mode 100644 index 000000000..ec24abcea --- /dev/null +++ b/components/signalement/hooks/useSignalementMapDiffDeletion.ts @@ -0,0 +1,75 @@ +import MapContext from "@/contexts/map"; +import MarkersContext from "@/contexts/markers"; +import ParcellesContext from "@/contexts/parcelles"; +import { useContext, useEffect } from "react"; +import { getPositionName } from "@/lib/positions-types-list"; + +export function useSignalementMapDiffDeletion(existingLocation: { + positions: any[]; + parcelles: string[]; +}) { + const { parcelles, positions } = existingLocation; + + const { addMarker, disableMarkers } = useContext(MarkersContext); + const { isStyleLoaded, setIsCadastreDisplayed, map } = useContext(MapContext); + const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = + useContext(ParcellesContext); + + useEffect(() => { + if (isStyleLoaded && parcelles?.length > 0) { + setIsCadastreDisplayed(true); + setShowSelectedParcelles(false); + setHighlightedParcelles(parcelles); + setIsDiffMode(true); + + return () => { + setIsCadastreDisplayed(false); + setShowSelectedParcelles(true); + setHighlightedParcelles([]); + setIsDiffMode(false); + }; + } + }, [ + isStyleLoaded, + setIsCadastreDisplayed, + parcelles, + setHighlightedParcelles, + setShowSelectedParcelles, + setIsDiffMode, + ]); + + useEffect(() => { + if (!map) { + return; + } + + const initCb = () => { + setHighlightedParcelles(parcelles); + }; + map.once("moveend", initCb); + + return () => { + map.off("moveend", initCb); + }; + }, [map, setHighlightedParcelles, parcelles]); + + useEffect(() => { + if (positions.length > 0) { + positions.forEach((position) => { + addMarker({ + id: position.id, + isMapMarker: true, + isDisabled: true, + color: "gray", + label: getPositionName(position.type), + longitude: position.point.coordinates[0], + latitude: position.point.coordinates[1], + }); + }); + } + + return () => { + disableMarkers(); + }; + }, [positions, addMarker, disableMarkers]); +} diff --git a/hooks/useSignalementMapDiff.ts b/components/signalement/hooks/useSignalementMapDiffUpdate.ts similarity index 86% rename from hooks/useSignalementMapDiff.ts rename to components/signalement/hooks/useSignalementMapDiffUpdate.ts index c53bb3aa9..b4819f7e6 100644 --- a/hooks/useSignalementMapDiff.ts +++ b/components/signalement/hooks/useSignalementMapDiffUpdate.ts @@ -9,12 +9,12 @@ import { getPositionName } from "@/lib/positions-types-list"; import { ActiveCardEnum, SignalementDiff } from "@/lib/utils/signalement"; import { useContext, useEffect, useState } from "react"; -export type SignalementMapDiffExistingLocation = { +export type SignalementMapDiffUpdateExistingLocation = { positions: any[]; parcelles: string[]; }; -export type SignalementMapDiffChangesRequested = { +export type SignalementMapDiffUpdateChangesRequested = { positions: any[]; parcelles: string[]; }; @@ -26,37 +26,28 @@ const mapInitialPositions = (positions: any[]) => signalementTypeMap[Signalement.type.LOCATION_TO_CREATE].foregroundColor, })); -export function useSignalementMapDiff( - existingLocation: SignalementMapDiffExistingLocation, - changesRequested: SignalementMapDiffChangesRequested +export function useSignalementMapDiffUpdate( + existingLocation: SignalementMapDiffUpdateExistingLocation, + changesRequested: SignalementMapDiffUpdateChangesRequested ) { const { positions, parcelles } = changesRequested; const { positions: existingPositions, parcelles: existingParcelles } = existingLocation; - const [activeCard, setActiveCard] = useState( - ActiveCardEnum.CHANGES - ); + const [activeCard, setActiveCard] = useState(); const { addMarker, disableMarkers } = useContext(MarkersContext); - const { map, isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); + const { isStyleLoaded, setIsCadastreDisplayed, map } = useContext(MapContext); const { - isCadastreVisible, setHighlightedParcelles, setShowSelectedParcelles, handleSetFeatureState, setIsDiffMode, } = useContext(ParcellesContext); - const [positionsToDisplay, setPositionsToDisplay] = useState( - mapInitialPositions(positions) - ); + const [positionsToDisplay, setPositionsToDisplay] = useState([]); useEffect(() => { - setPositionsToDisplay(mapInitialPositions(positions)); - }, [positions]); - - useEffect(() => { - if (isStyleLoaded) { + if (isStyleLoaded && parcelles?.length > 0) { setIsCadastreDisplayed(true); setShowSelectedParcelles(false); setIsDiffMode(true); @@ -69,15 +60,30 @@ export function useSignalementMapDiff( }; } }, [ - map, isStyleLoaded, setIsCadastreDisplayed, - parcelles, setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode, + parcelles, ]); + useEffect(() => { + if (!map) { + return; + } + + const initCb = () => { + setPositionsToDisplay(mapInitialPositions(positions)); + setActiveCard(ActiveCardEnum.CHANGES); + }; + map.once("moveend", initCb); + + return () => { + map.off("moveend", initCb); + }; + }, [map, positions]); + useEffect(() => { if (positionsToDisplay?.length > 0) { positionsToDisplay.forEach((position) => { @@ -99,10 +105,6 @@ export function useSignalementMapDiff( }, [positionsToDisplay, addMarker, disableMarkers]); useEffect(() => { - if (!isCadastreVisible) { - return; - } - switch (activeCard) { case ActiveCardEnum.INITIAL: setPositionsToDisplay( @@ -164,7 +166,6 @@ export function useSignalementMapDiff( break; } }, [ - isCadastreVisible, activeCard, parcelles, positions, diff --git a/components/signalement/signalement-diff/accordion-card.tsx b/components/signalement/signalement-diff/accordion-card.tsx index 7f5bccd62..c3f4c9939 100644 --- a/components/signalement/signalement-diff/accordion-card.tsx +++ b/components/signalement/signalement-diff/accordion-card.tsx @@ -5,17 +5,13 @@ import { Icon, Pane, } from "evergreen-ui"; -import TextDiff from "./text-diff"; -import { SignalementPositionDiff } from "./signalement-position-diff"; -import { SignalementParcelleDiff } from "./signalement-parcelle-diff"; -import { useRef } from "react"; +import { useRef, useState } from "react"; interface AccordionCardProps { title: string; backgroundColor?: string; isActive?: boolean; - onMouseEnter?: () => void; - onMouseLeave?: () => void; + onClick?: () => void; children: React.ReactNode; } @@ -23,19 +19,11 @@ export function AccordionCard({ title, backgroundColor, isActive, - onMouseEnter, - onMouseLeave, + onClick, children, }: AccordionCardProps) { const contentRef = useRef(null); - - const handleMouseEnter = () => { - onMouseEnter && onMouseEnter(); - }; - - const handleMouseLeave = () => { - onMouseLeave && onMouseLeave(); - }; + const [hover, setHover] = useState(false); return ( setHover(true)} + onMouseLeave={() => setHover(false)} + {...(isActive || hover ? { elevation: 3 } : {})} > {title} - {onMouseEnter && ( - - )} + {onClick && } void; - onMouseLeave?: () => void; + onClick?: () => void; backgroundColor?: string; isActive?: boolean; } @@ -39,8 +38,7 @@ export function SignalementNumeroDiffCard({ positions, parcelles, complement, - onMouseEnter, - onMouseLeave, + onClick, backgroundColor, isActive, }: SignalementNumeroDiffCardProps) { @@ -49,8 +47,7 @@ export function SignalementNumeroDiffCard({ title={title} backgroundColor={backgroundColor} isActive={isActive} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} + onClick={onClick} > {showDiff ? ( - + {parcelleDiff(parcelles, existingParcelles).map( ({ parcelle, diff }) => ( void; - onMouseLeave?: () => void; + onClick?: () => void; } export function SignalementToponymeDiffCard({ @@ -31,16 +30,14 @@ export function SignalementToponymeDiffCard({ positions, parcelles, isActive, - onMouseEnter, - onMouseLeave, + onClick, }: SignalementToponymeDiffCardProps) { return ( diff --git a/components/signalement/signalement-diff/text-diff.tsx b/components/signalement/signalement-diff/text-diff.tsx index 2ebb69c4e..7b27d3967 100644 --- a/components/signalement/signalement-diff/text-diff.tsx +++ b/components/signalement/signalement-diff/text-diff.tsx @@ -1,6 +1,8 @@ import { Paragraph, Text } from "evergreen-ui"; import React from "react"; import fastDiff from "fast-diff"; +import { signalementTypeMap } from "../signalement-type-badge"; +import { Signalement } from "@/lib/openapi-signalement"; interface TextDiffProps { from?: string; @@ -23,6 +25,10 @@ function TextDiff({ from, to }: TextDiffProps) { { - if (isStyleLoaded && parcelles?.length > 0) { - setIsCadastreDisplayed(true); - setShowSelectedParcelles(false); - setHighlightedParcelles(parcelles); - setIsDiffMode(true); - - return () => { - setIsCadastreDisplayed(false); - setShowSelectedParcelles(true); - setHighlightedParcelles([]); - setIsDiffMode(false); - }; - } - }, [ - isStyleLoaded, - setIsCadastreDisplayed, - parcelles, - setHighlightedParcelles, - setShowSelectedParcelles, - setIsDiffMode, - ]); - - useEffect(() => { - if (positions?.length > 0) { - positions.forEach((position: PositionSignalement & { id: string }) => { - addMarker({ - id: position.id, - isMapMarker: true, - isDisabled: true, - color: "gray", - longitude: position.point.coordinates[0], - latitude: position.point.coordinates[1], - label: getPositionName(position.type), - }); - }); - } - - return () => { - disableMarkers(); - }; - }, [positions, addMarker, disableMarkers]); + useSignalementMapDiffCreation( + signalement.changesRequested as NumeroChangesRequestedDTO + ); const onAccept = async () => { await VoiesService.createNumero(voie.id, { diff --git a/components/signalement/signalement-form/numero/signalement-delete-numero.tsx b/components/signalement/signalement-form/numero/signalement-delete-numero.tsx index 52b7a6557..4c1f741d9 100644 --- a/components/signalement/signalement-form/numero/signalement-delete-numero.tsx +++ b/components/signalement/signalement-form/numero/signalement-delete-numero.tsx @@ -1,13 +1,10 @@ -import React, { useContext, useEffect } from "react"; -import MapContext from "@/contexts/map"; +import React from "react"; import { Numero, NumerosService } from "@/lib/openapi-api-bal"; import { SignalementFormButtons } from "../signalement-form-buttons"; -import { getPositionName } from "@/lib/positions-types-list"; -import MarkersContext from "@/contexts/markers"; import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; -import ParcellesContext from "@/contexts/parcelles"; import { signalementTypeMap } from "../../signalement-type-badge"; import { Signalement } from "@/lib/openapi-signalement"; +import { useSignalementMapDiffDeletion } from "../../hooks/useSignalementMapDiffDeletion"; interface SignalementDeleteNumeroProps { existingLocation: Numero; @@ -24,55 +21,9 @@ function SignalementDeleteNumero({ handleClose, isLoading, }: SignalementDeleteNumeroProps) { - const { addMarker, disableMarkers } = useContext(MarkersContext); - const { isStyleLoaded, setIsCadastreDisplayed } = useContext(MapContext); - const { setHighlightedParcelles, setShowSelectedParcelles, setIsDiffMode } = - useContext(ParcellesContext); - const { numero, suffixe, voie, parcelles, positions } = existingLocation; - useEffect(() => { - if (isStyleLoaded && parcelles?.length > 0) { - setIsCadastreDisplayed(true); - setShowSelectedParcelles(false); - setHighlightedParcelles(parcelles); - setIsDiffMode(true); - - return () => { - setIsCadastreDisplayed(false); - setShowSelectedParcelles(true); - setHighlightedParcelles([]); - setIsDiffMode(false); - }; - } - }, [ - isStyleLoaded, - setIsCadastreDisplayed, - parcelles, - setHighlightedParcelles, - setShowSelectedParcelles, - setIsDiffMode, - ]); - - useEffect(() => { - if (positions.length > 0) { - positions.forEach((position) => { - addMarker({ - id: position.id, - isMapMarker: true, - isDisabled: true, - color: "gray", - label: getPositionName(position.type), - longitude: position.point.coordinates[0], - latitude: position.point.coordinates[1], - }); - }); - } - - return () => { - disableMarkers(); - }; - }, [positions, addMarker, disableMarkers]); + useSignalementMapDiffDeletion(existingLocation); const onAccept = async () => { await NumerosService.softDeleteNumero(existingLocation.id); diff --git a/components/signalement/signalement-form/numero/signalement-update-numero.tsx b/components/signalement/signalement-form/numero/signalement-update-numero.tsx index 00e3e8e28..145543120 100644 --- a/components/signalement/signalement-form/numero/signalement-update-numero.tsx +++ b/components/signalement/signalement-form/numero/signalement-update-numero.tsx @@ -13,7 +13,8 @@ import { SignalementFormButtons } from "../signalement-form-buttons"; import { ActiveCardEnum, detectChanges } from "@/lib/utils/signalement"; import { SignalementNumeroDiffCard } from "../../signalement-diff/signalement-numero-diff-card"; import { signalementTypeMap } from "../../signalement-type-badge"; -import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; +import { useSignalementMapDiffUpdate } from "@/components/signalement/hooks/useSignalementMapDiffUpdate"; +import { Alert } from "evergreen-ui"; interface SignalementUpdateNumeroProps { signalement: Signalement; @@ -47,7 +48,7 @@ function SignalementUpdateNumero({ voie: existingVoie, } = existingLocation; - const { activeCard, setActiveCard } = useSignalementMapDiff( + const { activeCard, setActiveCard } = useSignalementMapDiffUpdate( { positions: existingPositions, parcelles: existingParcelles }, { positions, parcelles } ); @@ -90,7 +91,7 @@ function SignalementUpdateNumero({ parcelles={{ to: existingParcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.INITIAL); }} /> @@ -123,7 +124,7 @@ function SignalementUpdateNumero({ from: existingParcelles, to: parcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.CHANGES); }} /> @@ -145,10 +146,15 @@ function SignalementUpdateNumero({ parcelles={{ to: parcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.FINAL); }} /> + {changes.voie && ( + + Le renommage de la voie affectera toutes les adresses de cette voie. + + )} void; } -const CONFIRMATION_DELAY = 5000; +const CONFIRMATION_DELAY = 8000; export function SignalementFormButtons({ isLoading, @@ -21,6 +22,7 @@ export function SignalementFormButtons({ "accept" | "reject" | null >(null); const timeOutRef = useRef(null); + const { pendingSignalementsCount } = useContext(SignalementContext); const handleActionToConfirm = (action: "accept" | "reject") => { setActionToConfirm(action); @@ -84,8 +86,10 @@ export function SignalementFormButtons({ iconAfter: BanCircleIcon, })} > - {actionToConfirm === "accept" ? "Accepter" : "Refuser"} et - passer au suivant + {actionToConfirm === "accept" ? "Accepter" : "Refuser"} et{" "} + {pendingSignalementsCount > 1 + ? "passer au suivant" + : "terminer"} - Annuler + Retour diff --git a/components/signalement/signalement-form/signalement-form.tsx b/components/signalement/signalement-form/signalement-form.tsx index 0e3509305..1dbe2da98 100644 --- a/components/signalement/signalement-form/signalement-form.tsx +++ b/components/signalement/signalement-form/signalement-form.tsx @@ -4,7 +4,12 @@ import { NumeroChangesRequestedDTO, Signalement, } from "@/lib/openapi-signalement"; -import { Numero, Toponyme, Voie } from "@/lib/openapi-api-bal"; +import { + ExtendedBaseLocaleDTO, + Numero, + Toponyme, + Voie, +} from "@/lib/openapi-api-bal"; import Form from "../../form"; import SignalementCreateNumero from "./numero/signalement-create-numero"; import SignalementUpdateNumero from "./numero/signalement-update-numero"; @@ -13,9 +18,12 @@ import SignalementUpdateToponyme from "./toponyme/signalement-update-toponyme"; import SignalementDeleteNumero from "./numero/signalement-delete-numero"; import MapContext from "@/contexts/map"; import { SignalementHeader } from "../signalement-header"; +import SignalementContext from "@/contexts/signalement"; +import { Paragraph } from "evergreen-ui"; interface SignalementFormProps { signalement: Signalement; + baseLocale: ExtendedBaseLocaleDTO; existingLocation: Voie | Toponyme | Numero; requestedToponyme?: Toponyme; onSubmit: (status: Signalement.status) => Promise; @@ -24,16 +32,22 @@ interface SignalementFormProps { function SignalementForm({ signalement, + baseLocale, existingLocation, requestedToponyme, onSubmit, onClose, }: SignalementFormProps) { const [isLoading, setIsLoading] = useState(false); - const { setViewport } = useContext(MapContext); + const { map } = useContext(MapContext); + const { pendingSignalementsCount } = useContext(SignalementContext); // Point the map to the location of the signalement useEffect(() => { + if (!map) { + return; + } + let pointTo = null; if ((existingLocation as Numero).positions?.length > 0) { @@ -61,13 +75,18 @@ function SignalementForm({ } if (pointTo) { - setViewport({ - latitude: pointTo.latitude, - longitude: pointTo.longitude, - zoom: 20, + map.flyTo({ + center: [pointTo.longitude, pointTo.latitude], + offset: [0, 0], + zoom: + signalement.type === Signalement.type.LOCATION_TO_CREATE || + signalement.existingLocation.type === ExistingLocation.type.NUMERO + ? 20 + : 16.5, + screenSpeed: 2, }); } - }, [existingLocation, signalement.changesRequested, setViewport]); + }, [existingLocation, signalement, map]); const handleSubmit = async (status: Signalement.status) => { try { @@ -98,7 +117,7 @@ function SignalementForm({ return Promise.resolve(); }} > - + {signalement.type === Signalement.type.LOCATION_TO_CREATE && ( )} + + Il reste {pendingSignalementsCount} signalement + {pendingSignalementsCount === 1 ? "" : "s"} à traiter + ); } diff --git a/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx b/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx index ccd64a90a..2adf0116e 100644 --- a/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx +++ b/components/signalement/signalement-form/toponyme/signalement-update-toponyme.tsx @@ -8,7 +8,7 @@ import { SignalementFormButtons } from "../signalement-form-buttons"; import { SignalementToponymeDiffCard } from "../../signalement-diff/signalement-toponyme-diff-card"; import { signalementTypeMap } from "../../signalement-type-badge"; import { ActiveCardEnum } from "@/lib/utils/signalement"; -import { useSignalementMapDiff } from "@/hooks/useSignalementMapDiff"; +import { useSignalementMapDiffUpdate } from "@/components/signalement/hooks/useSignalementMapDiffUpdate"; interface SignalementUpdateToponymeProps { signalement: Signalement; @@ -36,7 +36,7 @@ function SignalementUpdateToponyme({ const { nom, parcelles, positions } = signalement.changesRequested as ToponymeChangesRequestedDTO; - const { activeCard, setActiveCard } = useSignalementMapDiff( + const { activeCard, setActiveCard } = useSignalementMapDiffUpdate( { positions: existingPositions, parcelles: existingParcelles }, { positions, parcelles } ); @@ -62,7 +62,7 @@ function SignalementUpdateToponyme({ parcelles={{ to: existingParcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.INITIAL); }} /> @@ -85,7 +85,7 @@ function SignalementUpdateToponyme({ from: existingParcelles, to: parcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.CHANGES); }} /> @@ -101,7 +101,7 @@ function SignalementUpdateToponyme({ parcelles={{ to: parcelles, }} - onMouseEnter={() => { + onClick={() => { setActiveCard(ActiveCardEnum.FINAL); }} /> diff --git a/components/signalement/signalement-header.tsx b/components/signalement/signalement-header.tsx index 3623c5365..25a309b9c 100644 --- a/components/signalement/signalement-header.tsx +++ b/components/signalement/signalement-header.tsx @@ -2,17 +2,49 @@ import { Alert, Pane, Paragraph } from "evergreen-ui"; import SignalementTypeBadge from "./signalement-type-badge"; import { Signalement } from "@/lib/openapi-signalement"; import { getDuration } from "@/lib/utils/date"; +import { + ExtendedBaseLocaleDTO, + SignalementsService as SignalementsServiceBal, +} from "@/lib/openapi-api-bal"; +import { useEffect, useState } from "react"; interface SignalementHeaderProps { signalement: Signalement; + baseLocale: ExtendedBaseLocaleDTO; } const MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30; -export function SignalementHeader({ signalement }: SignalementHeaderProps) { +export function SignalementHeader({ + signalement, + baseLocale, +}: SignalementHeaderProps) { + const [author, setAuthor] = useState(); + const { + type, + createdAt, + source, + changesRequested, + status, + processedBy, + updatedAt, + } = signalement; + + useEffect(() => { + const fetchAuthor = async () => { + const author = await SignalementsServiceBal.getAuthor( + signalement.id, + baseLocale.id + ); + setAuthor(author); + }; + + fetchAuthor(); + }, [signalement, baseLocale]); + return ( } + title={} intent="info" padding={8} borderRadius={8} @@ -21,48 +53,55 @@ export function SignalementHeader({ signalement }: SignalementHeaderProps) { flexShrink={0} > - {Date.now() - new Date(signalement.createdAt).getTime() > - MONTH_IN_MS ? ( + {Date.now() - new Date(createdAt).getTime() > MONTH_IN_MS ? ( - Déposée le{" "} - {new Date(signalement.createdAt).toLocaleDateString()}{" "} + Déposée le {new Date(createdAt).toLocaleDateString()}{" "} ) : ( - Déposée il y a {getDuration(new Date(signalement.createdAt))}{" "} + Déposée il y a {getDuration(new Date(createdAt))}{" "} + + )} + {author && ( + + par{" "} + + {author.firstName} {author.lastName} + {" "} + {author.email && ( + {author.email} + )} )} - via {signalement.source.nom} + via {source.nom} - {signalement.changesRequested.comment && ( + {changesRequested.comment && ( Commentaire : - {signalement.changesRequested.comment} + {changesRequested.comment} )} - {signalement.status === Signalement.status.PROCESSED && ( + {status === Signalement.status.PROCESSED && ( <> - Acceptée le{" "} - {new Date(signalement.updatedAt).toLocaleDateString()} + Acceptée le {new Date(updatedAt).toLocaleDateString()} - via {signalement.processedBy.nom} + via {processedBy.nom} )} - {signalement.status === Signalement.status.IGNORED && ( + {status === Signalement.status.IGNORED && ( <> - Refusée le{" "} - {new Date(signalement.updatedAt).toLocaleDateString()} + Refusée le {new Date(updatedAt).toLocaleDateString()} - via {signalement.processedBy.nom} + via {processedBy.nom} )} diff --git a/components/signalement/signalement-joyride.tsx b/components/signalement/signalement-joyride.tsx new file mode 100644 index 000000000..3a3aa9797 --- /dev/null +++ b/components/signalement/signalement-joyride.tsx @@ -0,0 +1,136 @@ +import LocalStorageContext from "@/contexts/local-storage"; +import { Heading, Pane, Paragraph } from "evergreen-ui"; +import { useContext } from "react"; +import Joyride from "react-joyride"; + +const locale = { + skip: "Passer", + next: "Suivant", + back: "Précédent", + last: "Terminer", + close: "Fermer", +}; + +const styles = { + buttonNext: { + background: "#3366FF", + color: "white", + fontSize: 12, + }, + buttonSkip: { + fontSize: 12, + color: "#696f8c", + }, + buttonBack: { + fontSize: 12, + color: "#3366FF", + }, + buttonLast: { + background: "#3366FF", + color: "white", + fontSize: 12, + }, +}; + +export const steps = [ + { + target: "body", + placement: "center", + content: ( + + + Bienvenue sur la page de gestion des signalements + + + Nous allons vous guider à travers les différentes fonctionnalités de + cette page + + + ), + }, + { + target: "div[role='tablist'] > span:nth-child(1)", + content: ( + + + Cet onglet vous permet de consulter les signalements en attente de + traitement + + + ), + spotlightPadding: 5, + }, + { + target: "div[role='tablist'] > span:nth-child(2)", + content: ( + + + Celui-ci vous permet de consulter les signalements déjà traités + + + ), + spotlightPadding: 5, + }, + { + target: "input[placeholder='Rechercher un signalement']", + content: ( + + + Vous pouvez filtrer les signalements par nom en tapant dans cette + barre de recherche... + + + ), + spotlightPadding: 20, + }, + { + target: ".filter-button", + content: ( + + + ...ou par type (Création, Modification et Suppression) en cliquant sur + ce bouton + + + ), + spotlightPadding: 5, + }, + { + target: ".main-table-cell", + content: ( + + + Enfin séléctionnez un signalement soit via la liste... + + + ), + spotlightPadding: 5, + }, + { + target: ".maplibregl-marker", + content: ( + + Soit via la carte + + ), + spotlightPadding: 5, + }, +]; + +function SignalementJoyRide() { + const { productTour, setProductTour } = useContext(LocalStorageContext); + + return !productTour?.signalement ? ( + setProductTour({ ...productTour, signalement: true })} + /> + ) : null; +} + +export default SignalementJoyRide; diff --git a/components/signalement/signalement-list.tsx b/components/signalement/signalement-list.tsx index bc4ed7b41..c4b3e71bc 100644 --- a/components/signalement/signalement-list.tsx +++ b/components/signalement/signalement-list.tsx @@ -20,7 +20,7 @@ interface SignalementListProps { selectedSignalements: string[]; setSelectedSignalements: (ids: string[]) => void; onSelect: (id: string) => void; - onIgnore: (id: string) => void; + onIgnore: (id: string) => Promise; onToggleSelect: (ids: string[]) => void; filters: { type: Signalement.type[]; @@ -112,6 +112,7 @@ function SignalementList({ isShown={showFilters} >