From c24fe9d56baf02cb95762e99a316fed69d497dd2 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Oct 2023 15:38:06 +0200 Subject: [PATCH 1/7] feat: suppression --- .../intentions/routes/demande.routes.ts | 21 ++++- .../deleteDemande/deleteDemande.dep.ts | 5 + .../deleteDemande/deleteDemande.usecase.ts | 30 ++++++ .../submitDemande/submitDemande.usecase.ts | 2 +- .../submitDraftDemande.usecase.ts | 2 +- shared/client/intentions/intentions.client.ts | 5 + shared/client/intentions/intentions.schema.ts | 6 ++ shared/security/permissions.ts | 9 +- .../components/ConfirmationDelete.tsx | 65 +++++++++++++ .../MenuIntention.tsx | 0 .../intentionForm/InformationsBlock.tsx | 39 +------- .../intentionForm/IntentionForm.tsx | 93 +++++++++++++------ .../capaciteSection/CapaciteSection.tsx | 2 +- ui/app/(wrapped)/intentions/new/page.tsx | 2 +- ui/app/(wrapped)/intentions/page.client.tsx | 4 +- ui/app/(wrapped)/page.tsx | 2 +- ui/components/icons/arrowIcon.tsx | 14 +++ 17 files changed, 224 insertions(+), 77 deletions(-) create mode 100644 server/src/modules/intentions/usecases/deleteDemande/deleteDemande.dep.ts create mode 100644 server/src/modules/intentions/usecases/deleteDemande/deleteDemande.usecase.ts create mode 100644 ui/app/(wrapped)/intentions/components/ConfirmationDelete.tsx rename ui/app/(wrapped)/intentions/{menuIntention => components}/MenuIntention.tsx (100%) create mode 100644 ui/components/icons/arrowIcon.tsx diff --git a/server/src/modules/intentions/routes/demande.routes.ts b/server/src/modules/intentions/routes/demande.routes.ts index 1f1f7b139..23c8ee219 100644 --- a/server/src/modules/intentions/routes/demande.routes.ts +++ b/server/src/modules/intentions/routes/demande.routes.ts @@ -13,6 +13,7 @@ import { hasPermissionHandler } from "../../core"; import { countDemandes } from "../queries/countDemandes.query"; import { findDemande } from "../queries/findDemande.query"; import { findDemandes } from "../queries/findDemandes.query"; +import { deleteDemande } from "../usecases/deleteDemande/deleteDemande.usecase"; import { submitDemande } from "../usecases/submitDemande/submitDemande.usecase"; import { submitDraftDemande } from "../usecases/submitDraftDemande/submitDraftDemande.usecase"; @@ -21,7 +22,7 @@ export const demandeRoutes = ({ server }: { server: Server }) => { "/demande/submit", { schema: ROUTES_CONFIG.submitDemande, - preHandler: hasPermissionHandler("intentions/envoi"), + preHandler: hasPermissionHandler("intentions/ecriture"), }, async (request, response) => { const { demande } = request.body; @@ -39,7 +40,7 @@ export const demandeRoutes = ({ server }: { server: Server }) => { "/demande/draft", { schema: ROUTES_CONFIG.submitDraftDemande, - preHandler: hasPermissionHandler("intentions/envoi"), + preHandler: hasPermissionHandler("intentions/ecriture"), }, async (request, response) => { const { demande } = request.body; @@ -65,7 +66,7 @@ export const demandeRoutes = ({ server }: { server: Server }) => { const demande = await findDemande({ id: request.params.id, user }); if (!demande) return response.status(404).send(); - const scope = getPermissionScope(user.role, "intentions/envoi"); + const scope = getPermissionScope(user.role, "intentions/ecriture"); const canEdit = guardScope(scope?.default, { user: () => user.id === demande.createurId, region: () => user.codeRegion === demande.codeRegion, @@ -76,6 +77,20 @@ export const demandeRoutes = ({ server }: { server: Server }) => { } ); + server.delete( + "/demande/:id", + { + schema: ROUTES_CONFIG.deleteDemande, + preHandler: hasPermissionHandler("intentions/ecriture"), + }, + async (request, response) => { + const user = request.user; + if (!user) throw Boom.forbidden(); + await deleteDemande({ id: request.params.id, user }); + response.status(200).send(); + } + ); + server.get( "/demandes", { diff --git a/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.dep.ts b/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.dep.ts new file mode 100644 index 000000000..ec6a8aadb --- /dev/null +++ b/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.dep.ts @@ -0,0 +1,5 @@ +import { kdb } from "../../../../db/db"; + +export const deleteDemandeQuery = async (id: string) => { + await kdb.deleteFrom("demande").where("id", "=", id).execute(); +}; diff --git a/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.usecase.ts b/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.usecase.ts new file mode 100644 index 000000000..d8a85d0cf --- /dev/null +++ b/server/src/modules/intentions/usecases/deleteDemande/deleteDemande.usecase.ts @@ -0,0 +1,30 @@ +import Boom from "@hapi/boom"; +import { getPermissionScope, guardScope } from "shared"; + +import { RequestUser } from "../../../core/model/User"; +import { findOneDemande } from "../../repositories/findOneDemande.query"; +import { deleteDemandeQuery } from "./deleteDemande.dep"; + +export const deleteDemandeFactory = + (deps = { findOneDemande, deleteDemandeQuery }) => + async ({ + id, + user, + }: { + id: string; + user: Pick; + }) => { + const demande = await deps.findOneDemande(id); + if (!demande) throw Boom.notFound(); + + const scope = getPermissionScope(user.role, "intentions/ecriture"); + const isAllowed = guardScope(scope?.default, { + user: () => user.id === demande.createurId, + region: () => user.codeRegion === demande.codeRegion, + national: () => true, + }); + if (!isAllowed) throw Boom.forbidden(); + await deps.deleteDemandeQuery(demande.id); + }; + +export const deleteDemande = deleteDemandeFactory(); diff --git a/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts b/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts index 70e181149..fc43b38b4 100644 --- a/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts +++ b/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts @@ -74,7 +74,7 @@ export const [submitDemande, submitDemandeFactory] = inject( if (!dataEtablissement) throw Boom.badRequest("Code uai non valide"); if (!dataEtablissement.codeRegion) throw Boom.badData(); - const scope = getPermissionScope(user.role, "intentions/envoi"); + const scope = getPermissionScope(user.role, "intentions/ecriture"); const isAllowed = guardScope(scope?.default, { user: () => user.codeRegion === dataEtablissement.codeRegion && diff --git a/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts b/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts index 05f571e60..c812f61cf 100644 --- a/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts +++ b/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts @@ -74,7 +74,7 @@ export const [submitDraftDemande] = inject( if (!dataEtablissement) throw Boom.badRequest("Code uai non valide"); if (!dataEtablissement.codeRegion) throw Boom.badData(); - const scope = getPermissionScope(user.role, "intentions/envoi"); + const scope = getPermissionScope(user.role, "intentions/ecriture"); const isAllowed = guardScope(scope?.default, { user: () => user.codeRegion === dataEtablissement.codeRegion && diff --git a/shared/client/intentions/intentions.client.ts b/shared/client/intentions/intentions.client.ts index 448357304..19543933e 100644 --- a/shared/client/intentions/intentions.client.ts +++ b/shared/client/intentions/intentions.client.ts @@ -31,6 +31,11 @@ export const createIntentionsClient = (instance: AxiosInstance) => ({ url: "/demande/draft", instance, }), + deleteDemande: createClientMethod({ + method: "DELETE", + url: ({ params: { id } }) => `/demande/${id}`, + instance, + }), getDemande: createClientMethod({ method: "GET", url: ({ params: { id } }) => `/demande/${id}`, diff --git a/shared/client/intentions/intentions.schema.ts b/shared/client/intentions/intentions.schema.ts index 24134752b..b999df759 100644 --- a/shared/client/intentions/intentions.schema.ts +++ b/shared/client/intentions/intentions.schema.ts @@ -175,6 +175,12 @@ export const intentionsSchemas = { }), }, }, + deleteDemande: { + params: Type.Object({ id: Type.String() }), + response: { + 200: Type.Void(), + }, + }, getDemandes: { querystring: Type.Intersect([ FiltersSchema, diff --git a/shared/security/permissions.ts b/shared/security/permissions.ts index 30169841e..9cce6e4c5 100644 --- a/shared/security/permissions.ts +++ b/shared/security/permissions.ts @@ -11,8 +11,7 @@ export const PERMISSIONS = { admin: { "pilotage_reforme/lecture": { default: "national" }, "intentions/lecture": { default: "national", draft: "national" }, - "intentions/suppression": { default: "national" }, - "intentions/envoi": { default: "national" }, + "intentions/ecriture": { default: "national" }, }, pilote: { // "intentions/lecture": { default: "national", draft: "national" }, @@ -23,13 +22,11 @@ export const PERMISSIONS = { }, expert_region: { "intentions/lecture": { default: "region", draft: "region" }, - "intentions/suppression": { default: "region" }, - "intentions/envoi": { default: "region" }, + "intentions/ecriture": { default: "region" }, }, gestionnaire_region: { "intentions/lecture": { draft: "user", default: "region" }, - "intentions/suppression": { default: "user" }, - "intentions/envoi": { default: "user" }, + "intentions/ecriture": { default: "user" }, }, } satisfies { [R: string]: { diff --git a/ui/app/(wrapped)/intentions/components/ConfirmationDelete.tsx b/ui/app/(wrapped)/intentions/components/ConfirmationDelete.tsx new file mode 100644 index 000000000..c274abf0b --- /dev/null +++ b/ui/app/(wrapped)/intentions/components/ConfirmationDelete.tsx @@ -0,0 +1,65 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogOverlay, + Button, + Heading, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { FC, MouseEventHandler, useRef } from "react"; + +import { ArrowIcon } from "@/components/icons/arrowIcon"; + +export const ConfirmationDelete = ({ + Trigger, + onConfirm, +}: { + Trigger: FC<{ onClick: MouseEventHandler }>; + onConfirm: () => Promise; +}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const cancelRef = useRef(null); + + return ( + <> + + + + + + + + Confirmation de suppression + + + Êtes-vous sûr de vouloir supprimer la séléction ? + + + Cette action est irréversible, vous perdrez l’ensemble des + données associées à votre demande. + + + + + + + + + + + + ); +}; diff --git a/ui/app/(wrapped)/intentions/menuIntention/MenuIntention.tsx b/ui/app/(wrapped)/intentions/components/MenuIntention.tsx similarity index 100% rename from ui/app/(wrapped)/intentions/menuIntention/MenuIntention.tsx rename to ui/app/(wrapped)/intentions/components/MenuIntention.tsx diff --git a/ui/app/(wrapped)/intentions/intentionForm/InformationsBlock.tsx b/ui/app/(wrapped)/intentions/intentionForm/InformationsBlock.tsx index 6750da67d..ec4a7e54e 100644 --- a/ui/app/(wrapped)/intentions/intentionForm/InformationsBlock.tsx +++ b/ui/app/(wrapped)/intentions/intentionForm/InformationsBlock.tsx @@ -4,12 +4,11 @@ import { AlertIcon, AlertTitle, Box, - Button, Divider, Flex, - Text, UnorderedList, } from "@chakra-ui/react"; +import { ReactNode } from "react"; import { ApiType } from "shared"; import { api } from "../../../../api.client"; @@ -17,19 +16,14 @@ import { CapaciteSection } from "./capaciteSection/CapaciteSection"; import { TypeDemandeSection } from "./typeDemandeSection/TypeDemandeSection"; export const InformationsBlock = ({ - canEdit, errors, - isSubmitting, - onDraftSubmit, - isDraftSubmitting, formMetadata, + footerActions, }: { canEdit: boolean; errors?: Record; - isSubmitting?: boolean; - onDraftSubmit: () => void; - isDraftSubmitting?: boolean; formMetadata?: ApiType["metadata"]; + footerActions: ReactNode; }) => { return ( <> @@ -55,32 +49,7 @@ export const InformationsBlock = ({ )} - - - - (Phase d'enregistrement du 02 au 16 octobre) - - - - - - Pour soumission au vote du Conseil Régional - - + {footerActions} diff --git a/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx b/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx index 211a9361d..5aa5be31e 100644 --- a/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx +++ b/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx @@ -1,6 +1,7 @@ "use client"; -import { Box, Collapse, Container } from "@chakra-ui/react"; +import { DeleteIcon } from "@chakra-ui/icons"; +import { Box, Button, Collapse, Container, Text } from "@chakra-ui/react"; import { useMutation } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { usePathname, useRouter } from "next/navigation"; @@ -8,6 +9,7 @@ import { useEffect, useRef, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { ApiType } from "shared"; +import { ConfirmationDelete } from "@/app/(wrapped)/intentions/components/ConfirmationDelete"; import { IntentionForms, PartialIntentionForms, @@ -29,6 +31,7 @@ export const IntentionForm = ({ defaultValues: PartialIntentionForms; formMetadata?: ApiType["metadata"]; }) => { + const { push } = useRouter(); const pathname = usePathname(); const form = useForm({ defaultValues, @@ -41,12 +44,9 @@ export const IntentionForm = ({ const [errors, setErrors] = useState>(); const { isLoading: isSubmitting, mutateAsync: submit } = useMutation({ - mutationFn: ({ forms }: { forms: IntentionForms }) => - api - .submitDemande({ - body: { demande: { id: formId, ...forms } }, - }) - .call(), + mutationFn: (forms: IntentionForms) => + api.submitDemande({ body: { demande: { id: formId, ...forms } } }).call(), + onSuccess: () => push("/intentions"), onError: (e: AxiosError<{ errors: Record }>) => { const errors = e.response?.data.errors; setErrors(errors); @@ -57,16 +57,23 @@ export const IntentionForm = ({ useMutation({ mutationFn: ({ forms }: { forms: IntentionForms }) => api - .submitDraftDemande({ - body: { demande: { id: formId, ...forms } }, - }) + .submitDraftDemande({ body: { demande: { id: formId, ...forms } } }) .call(), + onSuccess: () => push("/intentions"), onError: (e: AxiosError<{ errors: Record }>) => { const errors = e.response?.data.errors; setErrors(errors); }, }); + const { isLoading: isDeleting, mutateAsync: deleteDemande } = useMutation({ + mutationFn: async () => { + if (!formId) return; + await api.deleteDemande({ params: { id: formId } }).call(); + }, + onSuccess: () => push("/intentions"), + }); + const [isFCIL, setIsFCIL] = useState( formMetadata?.formation?.isFCIL ?? false ); @@ -86,18 +93,6 @@ export const IntentionForm = ({ const onEditUaiCfdSection = () => setStep(1); - const { push } = useRouter(); - - const onSubmit = async () => { - await submit({ forms: getValues() }); - push("/intentions"); - }; - - const onDraftSubmit = handleSubmit(async () => { - await submitDraft({ forms: getValues() }); - push("/intentions"); - }); - useEffect(() => { if (isCFDUaiSectionValid(getValues())) { submitCFDUAISection(); @@ -120,7 +115,7 @@ export const IntentionForm = ({ bg="#E2E7F8" as="form" noValidate - onSubmit={handleSubmit(onSubmit)} + onSubmit={handleSubmit((values) => submit(values))} > + {formId && canEdit && ( + ( + + )} + /> + )} + + + + (Phase d'enregistrement du 02 au 16 octobre) + + + + + + Pour soumission au vote du Conseil Régional + + + + } /> diff --git a/ui/app/(wrapped)/intentions/intentionForm/capaciteSection/CapaciteSection.tsx b/ui/app/(wrapped)/intentions/intentionForm/capaciteSection/CapaciteSection.tsx index 38a9a013b..af8ff0a0b 100644 --- a/ui/app/(wrapped)/intentions/intentionForm/capaciteSection/CapaciteSection.tsx +++ b/ui/app/(wrapped)/intentions/intentionForm/capaciteSection/CapaciteSection.tsx @@ -80,7 +80,7 @@ export const CapaciteSection = () => { const typeDemande = watch("typeDemande"); const mixte = watch("mixte"); - const isTransfertApprentissage = watch("motif").includes( + const isTransfertApprentissage = watch("motif")?.includes( "transfert_apprentissage" ); diff --git a/ui/app/(wrapped)/intentions/new/page.tsx b/ui/app/(wrapped)/intentions/new/page.tsx index 2f0efe51d..bc9f10955 100644 --- a/ui/app/(wrapped)/intentions/new/page.tsx +++ b/ui/app/(wrapped)/intentions/new/page.tsx @@ -20,7 +20,7 @@ export default () => { if (isLoading && !!intentionId) return ; return ( - + {intentionId ? ( data && ( [0]["query"]; @@ -105,7 +105,7 @@ export const PageClient = () => { ); }; - const hasPermissionEnvoi = usePermission("intentions/envoi"); + const hasPermissionEnvoi = usePermission("intentions/ecriture"); if (isLoading) return ; diff --git a/ui/app/(wrapped)/page.tsx b/ui/app/(wrapped)/page.tsx index 8f4af3724..30da570e3 100644 --- a/ui/app/(wrapped)/page.tsx +++ b/ui/app/(wrapped)/page.tsx @@ -7,7 +7,7 @@ export const revalidate = 60; const fetchData = async () => { const notion = new NotionAPI(); const recordMap = await notion.getPage( - "Documentation-V0-Orion-32a009e0dabe48e890893f789162a451" + "Documentation-d-Orion-999f316583e9445191d4f62c37027c86" ); return recordMap; }; diff --git a/ui/components/icons/arrowIcon.tsx b/ui/components/icons/arrowIcon.tsx new file mode 100644 index 000000000..458e47a68 --- /dev/null +++ b/ui/components/icons/arrowIcon.tsx @@ -0,0 +1,14 @@ +import { createIcon } from "@chakra-ui/react"; + +export const ArrowIcon = createIcon({ + displayName: "arrowIcon", + viewBox: "0 0 32 32", + path: ( + + ), +}); From dbcf4808f4173d07a879b983d9ea91ce9f191868 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 2 Oct 2023 16:16:46 +0200 Subject: [PATCH 2/7] feat: form disabled --- .../intentions/[intentionId]/page.client.tsx | 2 +- .../{intentionForm => components}/InfoBox.tsx | 0 .../intentionForm/InformationsBlock.tsx | 15 ++- .../intentionForm/IntentionForm.tsx | 15 ++- .../capaciteSection/AmiCmaField.tsx | 76 +++++------ .../CapaciteApprentissageActuelleField.tsx | 3 +- .../CapaciteApprentissageColoreeField.tsx | 3 +- .../CapaciteApprentissageField.tsx | 3 +- .../CapaciteScolaireActuelleField.tsx | 3 +- .../CapaciteScolaireColoreeField.tsx | 3 +- .../capaciteSection/CapaciteScolaireField.tsx | 4 +- .../capaciteSection/CapaciteSection.tsx | 42 ++++-- .../capaciteSection/ColorationField.tsx | 8 +- .../capaciteSection/CommentaireField.tsx | 7 +- .../LibelleColorationField.tsx | 3 +- .../capaciteSection/MixteField.tsx | 74 ++++++----- .../capaciteSection/PoursuitePedagogique.tsx | 6 +- .../cfdUaiSection/CfdUaiSection.tsx | 22 ++-- .../typeDemandeSection/AutreMotifField.tsx | 65 +++++----- .../CompensationSection.tsx | 15 ++- .../typeDemandeSection/MotifField.tsx | 121 +++++++++--------- .../RentreeScolaireField.tsx | 3 +- .../typeDemandeSection/TypeDemandeField.tsx | 15 ++- .../typeDemandeSection/TypeDemandeSection.tsx | 18 ++- ui/app/(wrapped)/intentions/new/page.tsx | 4 +- 25 files changed, 301 insertions(+), 229 deletions(-) rename ui/app/(wrapped)/intentions/{intentionForm => components}/InfoBox.tsx (100%) diff --git a/ui/app/(wrapped)/intentions/[intentionId]/page.client.tsx b/ui/app/(wrapped)/intentions/[intentionId]/page.client.tsx index d18d109f9..ef8b132a7 100644 --- a/ui/app/(wrapped)/intentions/[intentionId]/page.client.tsx +++ b/ui/app/(wrapped)/intentions/[intentionId]/page.client.tsx @@ -24,7 +24,7 @@ export default ({ <> {data && ( ; formMetadata?: ApiType["metadata"]; footerActions: ReactNode; @@ -28,10 +29,10 @@ export const InformationsBlock = ({ return ( <> - + - + {errors && ( @@ -48,9 +49,11 @@ export const InformationsBlock = ({ )} - - {footerActions} - + {footerActions && ( + + {footerActions} + + )} ); diff --git a/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx b/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx index 5aa5be31e..e8e7899c5 100644 --- a/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx +++ b/ui/app/(wrapped)/intentions/intentionForm/IntentionForm.tsx @@ -21,12 +21,12 @@ import { CfdUaiSection } from "./cfdUaiSection/CfdUaiSection"; import { InformationsBlock } from "./InformationsBlock"; export const IntentionForm = ({ - canEdit = false, + disabled = true, formId, defaultValues, formMetadata, }: { - canEdit?: boolean; + disabled?: boolean; formId?: string; defaultValues: PartialIntentionForms; formMetadata?: ApiType["metadata"]; @@ -143,6 +143,7 @@ export const IntentionForm = ({ formMetadata={formMetadata} onEditUaiCfdSection={onEditUaiCfdSection} active={step === 1} + disabled={disabled} isFCIL={isFCIL} setIsFCIL={setIsFCIL} isCFDUaiSectionValid={isCFDUaiSectionValid} @@ -150,12 +151,12 @@ export const IntentionForm = ({ /> - {formId && canEdit && ( + {formId && ( ( @@ -164,7 +165,7 @@ export const IntentionForm = ({ color="red" borderColor="red" mr="auto" - isDisabled={!canEdit} + isDisabled={disabled} isLoading={isDeleting} variant="secondary" leftIcon={} @@ -176,7 +177,7 @@ export const IntentionForm = ({ )}