Repository: ABC-TransitionBasCarbone/bilan-carbone
Pull request #724: Etq 493 select multiple posts and subposts for manual fe
Author: login: jilink
Created: 2025-03-04T09:10:05Z | Merged: 2025-03-10T09:03:51Z

https://github.com/ABC-TransitionBasCarbone/bilan-carbone/issues/493

Je vous laisse me dire si tout est ok niveau ux/ui et dans le code ! N'hésitez pas si vous voulez qu'on en discute

~~J'ai juste une petite erreur Typescript dont je n'arrive pas à me débarasser si jamais quelqu'un a déjà rencontré ce problème~~
~~Dans EmissionFactorForm.tsx (ligne 77) lorsque j'appelle~~
~~` <MultiplePosts form={form} control={control} />`~~
~~form n'est pas au bon type alors que je l'ai typé exactement comme dans les autres composant ...~~
-> C'est reglé merci Louis !


Diff:
diff --git a/src/components/base/MultiSelect.tsx b/src/components/base/MultiSelect.tsx
new file mode 100644
index 0000000000..b48741b3fc
--- /dev/null
+++ b/src/components/base/MultiSelect.tsx
@@ -0,0 +1,61 @@
+import { MenuItem, SelectChangeEvent, SelectProps } from '@mui/material'
+import { useMemo, useState } from 'react'
+import { Select } from './Select'
+
+interface MultiSelectProps {
+  icon?: React.ReactNode
+  placeholder?: string
+  iconPosition?: 'before' | 'after'
+  options: { label: string; value: string }[]
+  onChange: (value: string[]) => void
+  translation: (slug: string) => string
+}
+
+export const MultiSelect = ({
+  name,
+  label,
+  value,
+  onChange,
+  options,
+  translation,
+  placeholder,
+  ...selectProps
+}: Omit<SelectProps, 'onChange'> & MultiSelectProps) => {
+  const [selected, setSelected] = useState<string[]>(
+    typeof value === 'string' ? (value.split(',') as string[]) : (value as string[]),
+  )
+
+  const translatedSelected = useMemo(() => selected.map((v) => translation(v)), [selected, translation])
+
+  const handleChange = (event: SelectChangeEvent<unknown>) => {
+    const {
+      target: { value },
+    } = event
+
+    const tmpSelected: string[] = typeof value === 'string' ? (value.split(',') as string[]) : (value as string[])
+    onChange(tmpSelected)
+    setSelected(tmpSelected)
+  }
+  return (
+    <Select
+      multiple
+      name={name}
+      label={label}
+      value={selected || []}
+      onChange={handleChange}
+      {...selectProps}
+      renderValue={() => {
+        if (translatedSelected.length === 0) {
+          return <em>{placeholder}</em>
+        }
+        return translatedSelected.join(', ')
+      }}
+    >
+      {options.map((option) => (
+        <MenuItem key={option.value} value={option.value}>
+          {option.label}
+        </MenuItem>
+      ))}
+    </Select>
+  )
+}
diff --git a/src/components/emissionFactor/Form/EmissionFactorForm.tsx b/src/components/emissionFactor/Form/EmissionFactorForm.tsx
index abec14b866..ced73bca86 100644
--- a/src/components/emissionFactor/Form/EmissionFactorForm.tsx
+++ b/src/components/emissionFactor/Form/EmissionFactorForm.tsx
@@ -4,7 +4,6 @@ import LinkButton from '@/components/base/LinkButton'
 import LoadingButton from '@/components/base/LoadingButton'
 import { FormSelect } from '@/components/form/Select'
 import { FormTextField } from '@/components/form/TextField'
-import { Post } from '@/services/posts'
 import { EmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
 import { MenuItem } from '@mui/material'
 import { Unit } from '@prisma/client'
@@ -13,11 +12,10 @@ import { useTranslations } from 'next-intl'
 import { useMemo } from 'react'
 import { Control, UseFormReturn } from 'react-hook-form'
 import DetailedGES from './DetailedGES'
-import Posts from './Posts'
+import MultiplePosts from './MultiplePosts'
 
 interface Props<T extends EmissionFactorCommand> {
   form: UseFormReturn<T>
-  post?: Post
   detailedGES?: boolean
   error: string
   hasParts: boolean
@@ -29,7 +27,6 @@ interface Props<T extends EmissionFactorCommand> {
 
 const EmissionFactorForm = <T extends EmissionFactorCommand>({
   form,
-  post,
   detailedGES,
   error,
   hasParts,
@@ -77,7 +74,7 @@ const EmissionFactorForm = <T extends EmissionFactorCommand>({
         partsCount={partsCount}
         setPartsCount={setPartsCount}
       />
-      <Posts form={form} post={post} />
+      <MultiplePosts form={form} />
       <FormTextField control={control} translation={t} name="comment" label={t('comment')} multiline rows={2} />
       <div className={classNames({ ['justify-between']: button === 'update' })}>
         {button === 'update' && (
diff --git a/src/components/emissionFactor/Form/MultiplePosts.tsx b/src/components/emissionFactor/Form/MultiplePosts.tsx
new file mode 100644
index 0000000000..8383f998de
--- /dev/null
+++ b/src/components/emissionFactor/Form/MultiplePosts.tsx
@@ -0,0 +1,88 @@
+import HelpIcon from '@/components/base/HelpIcon'
+import { Select } from '@/components/base/Select'
+import GlossaryModal from '@/components/modals/GlossaryModal'
+import { Post } from '@/services/posts'
+import { EmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
+import { Box, FormControl, FormHelperText, MenuItem, SelectChangeEvent } from '@mui/material'
+import { SubPost } from '@prisma/client'
+import { useTranslations } from 'next-intl'
+import { useMemo, useState } from 'react'
+import { Control, Controller, FieldPath, UseFormReturn, UseFormSetValue } from 'react-hook-form'
+import Posts from './Posts'
+import styles from './Posts.module.css'
+
+interface Props<T extends EmissionFactorCommand> {
+  form: UseFormReturn<T>
+}
+
+const MultiplePosts = <T extends EmissionFactorCommand>({ form }: Props<T>) => {
+  const t = useTranslations('emissionFactors.create')
+  const tPost = useTranslations('emissionFactors.post')
+  const tGlossary = useTranslations('emissionFactors.create.glossary')
+
+  const control = form.control as Control<EmissionFactorCommand>
+  const setValue = form.setValue as UseFormSetValue<EmissionFactorCommand>
+
+  const posts: Record<Post, SubPost[]> = (form.watch('subPosts' as FieldPath<T>) as Record<Post, SubPost[]>) || {}
+  const [glossary, setGlossary] = useState('')
+
+  const postSelection: Post[] = useMemo(
+    () =>
+      Object.keys(Post)
+        .sort((a, b) => tPost(a).localeCompare(tPost(b)))
+        .filter((p) => !Object.keys(posts).includes(p)) as Post[],
+    [posts],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    const currentSubPosts = { ...posts, [selectedPost]: [] }
+    setValue('subPosts', currentSubPosts)
+  }
+
+  return (
+    <div className="flex-col">
+      {Object.keys(posts).map((postKey) => (
+        <Box key={postKey} className={styles.postContainer}>
+          <Posts postOptions={postSelection} form={form} post={postKey as Post} subPosts={posts[postKey as Post]} />
+        </Box>
+      ))}
+
+      <FormControl className={styles.selectForm} error={Object.keys(posts).length === 0}>
+        <Select
+          name={'post'}
+          onChange={handleSelectPost}
+          data-testid="emission-factor-post"
+          label={t('posts')}
+          fullWidth
+          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
+          iconPosition="after"
+        >
+          {postSelection.map((post) => (
+            <MenuItem key={post} value={post}>
+              {tPost(post)}
+            </MenuItem>
+          ))}
+        </Select>
+      </FormControl>
+
+      <Controller
+        name={'subPosts' as FieldPath<T>}
+        control={control}
+        render={({ fieldState: { error } }) => (
+          <FormControl error={!!error}>
+            {error && error.message && <FormHelperText color="red">{t('validation.' + error.message)}</FormHelperText>}
+          </FormControl>
+        )}
+      />
+
+      {glossary && (
+        <GlossaryModal glossary={glossary} label="emission-factor-post" t={tGlossary} onClose={() => setGlossary('')}>
+          {tGlossary(`${glossary}Description`)}
+        </GlossaryModal>
+      )}
+    </div>
+  )
+}
+
+export default MultiplePosts
diff --git a/src/components/emissionFactor/Form/Posts.module.css b/src/components/emissionFactor/Form/Posts.module.css
new file mode 100644
index 0000000000..a8d2df00a6
--- /dev/null
+++ b/src/components/emissionFactor/Form/Posts.module.css
@@ -0,0 +1,16 @@
+.deleteButton {
+  min-height: fit-content;
+  height: 3.5rem;
+}
+
+.multiSelectForm {
+  width: 50%;
+}
+
+.selectForm {
+  width: 40%;
+}
+
+.postContainer {
+  margin-bottom: 0.5rem;
+}
diff --git a/src/components/emissionFactor/Form/Posts.tsx b/src/components/emissionFactor/Form/Posts.tsx
index c1573688cd..75a02eed27 100644
--- a/src/components/emissionFactor/Form/Posts.tsx
+++ b/src/components/emissionFactor/Form/Posts.tsx
@@ -1,30 +1,38 @@
 'use client'
 
-import HelpIcon from '@/components/base/HelpIcon'
+import Button from '@/components/base/Button'
+import { MultiSelect } from '@/components/base/MultiSelect'
 import { Select } from '@/components/base/Select'
-import { FormSelect } from '@/components/form/Select'
-import GlossaryModal from '@/components/modals/GlossaryModal'
 import { Post, subPostsByPost } from '@/services/posts'
 import { EmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
-import { FormControl, MenuItem } from '@mui/material'
+import { getPost } from '@/utils/post'
+import DeleteIcon from '@mui/icons-material/Delete'
+import { Box, FormControl, MenuItem, SelectChangeEvent } from '@mui/material'
 import { SubPost } from '@prisma/client'
 import { useTranslations } from 'next-intl'
 import { useMemo, useState } from 'react'
-import { Control, UseFormReturn, UseFormSetValue } from 'react-hook-form'
+import { Path, UseFormReturn, UseFormSetValue } from 'react-hook-form'
+import styles from './Posts.module.css'
 
 interface Props<T extends EmissionFactorCommand> {
   post?: Post
+  postOptions: Post[]
+  subPosts?: SubPost[]
   form: UseFormReturn<T>
 }
 
-const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Props<T>) => {
+const Posts = <T extends EmissionFactorCommand>({
+  form,
+  subPosts: initalSubPosts,
+  post: initialPost,
+  postOptions,
+}: Props<T>) => {
   const t = useTranslations('emissionFactors.create')
-  const tGlossary = useTranslations('emissionFactors.create.glossary')
   const tPost = useTranslations('emissionFactors.post')
-  const [post, setPost] = useState<Post | undefined>(initalPost)
-  const [glossary, setGlossary] = useState('')
+  const [selectedSubPosts, setSelectedSubPosts] = useState<SubPost[] | undefined>(initalSubPosts)
+
+  const [post, setPost] = useState<Post | undefined>(getPost(initalSubPosts?.[0]) || initialPost)
 
-  const control = form.control as Control<EmissionFactorCommand>
   const setValue = form.setValue as UseFormSetValue<EmissionFactorCommand>
 
   const posts = useMemo(() => Object.keys(Post).sort((a, b) => tPost(a).localeCompare(tPost(b))), [tPost])
@@ -33,55 +41,86 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subPost) => ({ label: tPost(subPost), value: subPost })),
+    [subPosts],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: Record<Post, SubPost[]> =
+      (form.getValues('subPosts' as Path<T>) as Record<Post, SubPost[]>) || {}
+    if (post) {
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: Record<Post, SubPost[]> =
+      (form.getValues('subPosts' as Path<T>) as Record<Post, SubPost[]>) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: Record<Post, SubPost[]> =
+      (form.getValues('subPosts' as Path<T>) as Record<Post, SubPost[]>) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box className="w100 align-end justify-between">
+      <FormControl className={styles.selectForm}>
         <Select
-          name="Post"
-          data-testid="emission-factor-post"
+          name="post"
           labelId="post-select-label"
           value={post || ''}
-          onChange={(event) => {
-            // @ts-expect-error: Force undefined to trigger error if not filled
-            setValue('subPost', undefined)
-            setPost(event.target.value as Post)
-          }}
+          onChange={handleSelectPost}
           label={t('post')}
-          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
-          iconPosition="after"
         >
           {posts.map((post) => (
-            <MenuItem key={post} value={post}>
+            <MenuItem disabled={!postOptions.includes(post as Post)} key={post} value={post}>
               {tPost(post)}
             </MenuItem>
           ))}
         </Select>
       </FormControl>
-      <FormSelect
-        data-testid="emission-factor-subPost"
-        control={control}
-        translation={t}
-        label={t('subPost')}
-        name="subPost"
+      <FormControl className={styles.multiSelectForm} error={selectedSubPosts?.length === 0}>
+        <MultiSelect
+          name="subPosts"
+          data-testid="emission-factor-subPost"
+          labelId="subpost-select-label"
+          value={selectedSubPosts || []}
+          onChange={handleSelectSubPost}
+          label={t('subPost')}
+          options={translatedSubPosts}
+          placeholder="placeholdertest"
+          translation={tPost}
+        />
+      </FormControl>
+      <Button
+        className={styles.deleteButton}
+        data-testid="delete-site-button"
+        title={t('delete')}
+        aria-label={t('delete')}
+        onClick={handleDelete}
       >
-        {subPosts.length === 0 ? (
-          <MenuItem value="" disabled>
-            {t('missingPost')}
-          </MenuItem>
-        ) : (
-          subPosts.map((subPost) => (
-            <MenuItem key={subPost} value={subPost}>
-              {tPost(subPost)}
-            </MenuItem>
-          ))
-        )}
-      </FormSelect>
-      {glossary && (
-        <GlossaryModal glossary={glossary} label="emission-factor-post" t={tGlossary} onClose={() => setGlossary('')}>
-          {tGlossary(`${glossary}Description`)}
-        </GlossaryModal>
-      )}
-    </>
+        <DeleteIcon />
+      </Button>
+    </Box>
   )
 }
 
diff --git a/src/components/emissionFactor/edit/Form.tsx b/src/components/emissionFactor/edit/Form.tsx
index ebe1d11a2d..b01a56452d 100644
--- a/src/components/emissionFactor/edit/Form.tsx
+++ b/src/components/emissionFactor/edit/Form.tsx
@@ -3,16 +3,18 @@
 import Form from '@/components/base/Form'
 import { gazKeys } from '@/constants/emissions'
 import { DetailedEmissionFactor } from '@/db/emissionFactors'
-import { Post, subPostsByPost } from '@/services/posts'
+import { Post } from '@/services/posts'
 import { updateEmissionFactorCommand } from '@/services/serverFunctions/emissionFactor'
 import {
   maxParts,
   UpdateEmissionFactorCommand,
   UpdateEmissionFactorCommandValidation,
 } from '@/services/serverFunctions/emissionFactor.command'
+import { getPost } from '@/utils/post'
 import { zodResolver } from '@hookform/resolvers/zod'
+import { SubPost } from '@prisma/client'
 import { useRouter } from 'next/navigation'
-import { FormEvent, useState } from 'react'
+import { FormEvent, useMemo, useState } from 'react'
 import { useForm } from 'react-hook-form'
 import EmissionFactorForm from '../Form/EmissionFactorForm'
 
@@ -52,11 +54,19 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
-  }
+  const subPostObject = useMemo(() => {
+    return emissionFactor.subPosts.reduce<Record<Post, SubPost[]>>(
+      (acc, subPost) => {
+        const post = getPost(subPost)
+        if (post) {
+          acc[post] = acc[post] ?? []
+          acc[post].push(subPost)
+        }
+        return acc
+      },
+      {} as Record<Post, SubPost[]>,
+    )
+  }, [emissionFactor.subPosts])
 
   const detailedGES =
     isDecomposed(emissionFactor) || (emissionFactor.emissionFactorParts || []).some((part) => isDecomposed(part))
@@ -71,7 +81,7 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
       attribute: emissionFactor?.metaData[0].attribute || '',
       source: emissionFactor?.source || '',
       unit: emissionFactor?.unit || undefined,
-      subPost: emissionFactor?.subPosts[0] || undefined,
+      subPosts: subPostObject,
       ...getGazValues(emissionFactor),
       totalCo2: emissionFactor?.totalCo2 || 0,
       parts: buildParts(emissionFactor, partsCount),
@@ -81,6 +91,7 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
 
   const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
+
     form.setValue('parts', hasParts ? form.getValues('parts').slice(0, partsCount) : [])
     form.handleSubmit(async (data) => {
       const result = await updateEmissionFactorCommand(data)
@@ -99,7 +110,6 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
       <EmissionFactorForm
         form={form}
         detailedGES={detailedGES}
-        post={post}
         error={error}
         hasParts={hasParts}
         setHasParts={setHasParts}
diff --git a/src/components/emissionFactor/new/Form.tsx b/src/components/emissionFactor/new/Form.tsx
index ad7550eaba..c75c202cee 100644
--- a/src/components/emissionFactor/new/Form.tsx
+++ b/src/components/emissionFactor/new/Form.tsx
@@ -38,7 +38,6 @@ const NewEmissionFactorForm = () => {
     form.setValue('parts', hasParts ? form.getValues('parts').slice(0, partsCount) : [])
     form.handleSubmit(async (data) => {
       const result = await createEmissionFactorCommand(data)
-
       if (result) {
         setError(result)
       } else {
diff --git a/src/db/emissionFactors.ts b/src/db/emissionFactors.ts
index 904674c5bd..6bb019f9da 100644
--- a/src/db/emissionFactors.ts
+++ b/src/db/emissionFactors.ts
@@ -1,4 +1,5 @@
 import { UpdateEmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
+import { flattenSubposts } from '@/utils/post'
 import { EmissionFactorStatus, Import, Unit, type Prisma } from '@prisma/client'
 import { Session } from 'next-auth'
 import { prismaClient } from './client'
@@ -105,7 +106,7 @@ export const createEmissionFactor = (emissionFactor: Prisma.EmissionFactorCreate
 export const updateEmissionFactor = async (
   session: Session,
   local: string,
-  { id, name, unit, attribute, comment, parts, subPost, ...command }: UpdateEmissionFactorCommand,
+  { id, name, unit, attribute, comment, parts, subPosts, ...command }: UpdateEmissionFactorCommand,
 ) => {
   const emissionFactor = {
     ...command,
@@ -114,7 +115,7 @@ export const updateEmissionFactor = async (
     reliability: 5,
     organization: { connect: { id: session?.user.organizationId as string } },
     unit: unit as Unit,
-    subPosts: [subPost],
+    subPosts: flattenSubposts(subPosts),
   }
   await prismaClient.$transaction(async (transaction) => {
     await transaction.emissionFactor.update({
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d9a15c7326..b575706cdb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -539,10 +539,12 @@
       "otherGES": "Other gases (in kgCO₂e)",
       "part": "Part",
       "post": "Post",
+      "posts": "Posts",
       "missingPost": "Please select a post",
       "subPost": "Sub Post",
       "partType": "Part type",
       "comment": "Comments",
+      "delete": "Delete",
       "glossary": {
         "title": "Glossary",
         "detailedGES": "Breakdown of GHG",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 0e367971da..03d9fd8519 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -540,9 +540,11 @@
       "otherGES": "Autres gaz (en kgCO₂e)",
       "part": "Composante",
       "post": "Poste",
+      "posts": "Postes",
       "missingPost": "Veuillez selectionner un poste",
       "subPost": "Sous Poste",
       "comment": "Commentaires",
+      "delete": "Supprimer",
       "glossary": {
         "title": "Glossaire",
         "detailedGES": "Décomposition des GES",
diff --git a/src/services/serverFunctions/emissionFactor.command.ts b/src/services/serverFunctions/emissionFactor.command.ts
index a493a47546..848420a7e2 100644
--- a/src/services/serverFunctions/emissionFactor.command.ts
+++ b/src/services/serverFunctions/emissionFactor.command.ts
@@ -38,7 +38,23 @@ export const EmissionFactorCommandValidation = z.intersection(
       })
       .min(0, 'totalCo2'),
     attribute: z.string().optional(),
-    subPost: z.nativeEnum(SubPost, { required_error: 'subPost' }),
+    subPosts: z.record(z.array(z.nativeEnum(SubPost)).min(1), { required_error: 'type' }).superRefine((val, ctx) => {
+      if (Object.keys(val).length === 0) {
+        ctx.addIssue({
+          message: 'type',
+          code: z.ZodIssueCode.custom,
+        })
+        return false
+      }
+
+      if (Object.values(val).some((arr) => arr.length === 0)) {
+        ctx.addIssue({
+          message: 'subPost',
+          code: z.ZodIssueCode.custom,
+        })
+        return false
+      }
+    }),
     comment: z.string().optional(),
     parts: z
       .array(
diff --git a/src/services/serverFunctions/emissionFactor.ts b/src/services/serverFunctions/emissionFactor.ts
index 133d316a37..b50e7753e2 100644
--- a/src/services/serverFunctions/emissionFactor.ts
+++ b/src/services/serverFunctions/emissionFactor.ts
@@ -11,6 +11,7 @@ import {
 } from '@/db/emissionFactors'
 import { getUserByEmail } from '@/db/user'
 import { getLocale } from '@/i18n/locale'
+import { flattenSubposts } from '@/utils/post'
 import { EmissionFactorStatus, Import, Unit } from '@prisma/client'
 import { auth } from '../auth'
 import { NOT_AUTHORIZED } from '../permissions/check'
@@ -106,7 +107,7 @@ export const createEmissionFactorCommand = async ({
   attribute,
   comment,
   parts,
-  subPost,
+  subPosts,
   ...command
 }: EmissionFactorCommand) => {
   const session = await auth()
@@ -132,7 +133,7 @@ export const createEmissionFactorCommand = async ({
     reliability: 5,
     organization: { connect: { id: user.organizationId } },
     unit: unit as Unit,
-    subPosts: [subPost],
+    subPosts: flattenSubposts(subPosts),
     metaData: {
       create: {
         language: local,
diff --git a/src/tests/end-to-end/app/create-emission-factor.cy.ts b/src/tests/end-to-end/app/create-emission-factor.cy.ts
index 461ad499c0..cdabc0c0bc 100644
--- a/src/tests/end-to-end/app/create-emission-factor.cy.ts
+++ b/src/tests/end-to-end/app/create-emission-factor.cy.ts
@@ -20,6 +20,7 @@ describe('Create emission factor', () => {
     cy.get('[data-value="Energies"]').click()
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
@@ -92,6 +93,7 @@ describe('Create emission factor', () => {
     cy.get('[data-value="Energies"]').click()
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
@@ -161,6 +163,7 @@ describe('Create emission factor', () => {
     cy.get('[data-value="Energies"]').click()
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
@@ -256,6 +259,7 @@ describe('Create emission factor', () => {
     cy.get('[data-value="Energies"]').click()
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
@@ -434,6 +438,7 @@ describe('Create emission factor', () => {
     cy.get('[data-value="Energies"]').click()
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
diff --git a/src/tests/end-to-end/app/edit-emission-factor.cy.ts b/src/tests/end-to-end/app/edit-emission-factor.cy.ts
index 2b575c630a..95695db133 100644
--- a/src/tests/end-to-end/app/edit-emission-factor.cy.ts
+++ b/src/tests/end-to-end/app/edit-emission-factor.cy.ts
@@ -57,8 +57,9 @@ describe('Edit emission factor', () => {
 
     cy.getByTestId('emission-factor-post').click()
     cy.get('[data-value="Energies"]').click()
-    cy.getByTestId('emission-factor-subPost').click()
+    cy.getByTestId('emission-factor-subPost').first().click()
     cy.get('[data-value="Electricite"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
 
@@ -122,8 +123,9 @@ describe('Edit emission factor', () => {
 
     cy.getByTestId('emission-factor-post').click()
     cy.get('[data-value="Deplacements"]').click()
-    cy.getByTestId('emission-factor-subPost').click()
+    cy.getByTestId('emission-factor-subPost').eq(1).click()
     cy.get('[data-value="DeplacementsDomicileTravail"]').click()
+    cy.get('body').type('{esc}')
 
     cy.getByTestId('emission-factor-valid-button').click()
     cy.wait('@update')
diff --git a/src/utils/post.ts b/src/utils/post.ts
new file mode 100644
index 0000000000..b7c805d6fe
--- /dev/null
+++ b/src/utils/post.ts
@@ -0,0 +1,12 @@
+import { Post, subPostsByPost } from '@/services/posts'
+import { SubPost } from '@prisma/client'
+
+export const getPost = (subPost?: SubPost) =>
+  subPost
+    ? (Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post)
+    : undefined
+
+export const flattenSubposts = (subPosts: Record<Post, SubPost[]>) =>
+  Object.keys(subPosts)
+    .map((post) => (subPosts?.[post as Post] || []).flat())
+    .flat()


Reviews:
user: login: RomainCrevecoeur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:23:04Z
user: login: jilink
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:24:45Z
user: login: RomainCrevecoeur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:29:00Z
user: login: jilink
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:30:49Z
user: login: RomainCrevecoeur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T14:02:15Z
user: login: RomainCrevecoeur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T14:08:34Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:15:09Z
user: login: RomainCrevecoeur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T14:17:45Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:48:52Z
user: login: RomainCrevecoeur
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T15:50:15Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:57:37Z
user: login: jilink
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T16:06:05Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:00:05Z
user: login: jilink
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:27:30Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T16:05:44Z
user: login: jilink
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T08:05:34Z
user: login: fortinlouis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T08:11:47Z
user: login: fortinlouis
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-10T08:23:36Z

Review comments:
diff_hunk: @@ -81,6 +90,8 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
 
   const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
+    console.log('onSubmit', form.getValues())
path: src/components/emissionFactor/edit/Form.tsx
user: login: RomainCrevecoeur
body: Est ce que tu peux supprimer les console log ?
created_at: 2025-03-04T10:23:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +90,8 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
 
   const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
+    console.log('onSubmit', form.getValues())
path: src/components/emissionFactor/edit/Form.tsx
user: login: jilink
body: Yep My bad je pensais avoir tout supprimé 
created_at: 2025-03-04T10:24:45Z
author_association: COLLABORATOR
diff_hunk: @@ -81,6 +90,8 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
 
   const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
+    console.log('onSubmit', form.getValues())
path: src/components/emissionFactor/edit/Form.tsx
user: login: RomainCrevecoeur
body: Pour le fonctionnement des commentaires on a décidé que ce soit l'auteur du commentaire qui résolve les conversation comme ça ça permet de repasser dessus ensuite
created_at: 2025-03-04T10:28:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +90,8 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
 
   const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
     event.preventDefault()
+    console.log('onSubmit', form.getValues())
path: src/components/emissionFactor/edit/Form.tsx
user: login: jilink
body: ça marche tu fais bien de me le dire 👍
created_at: 2025-03-04T10:30:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,106 @@
+import HelpIcon from '@/components/base/HelpIcon'
+import { Select } from '@/components/base/Select'
+import GlossaryModal from '@/components/modals/GlossaryModal'
+import { Post, PostObject } from '@/services/posts'
+import { EmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
+import { Box, FormControl, FormHelperText, MenuItem, SelectChangeEvent } from '@mui/material'
+import { useTranslations } from 'next-intl'
+import { useEffect, useMemo, useState } from 'react'
+import { Control, Controller, FieldPath, UseFormReturn, UseFormSetValue } from 'react-hook-form'
+import Posts from './Posts'
+
+interface Props<T extends EmissionFactorCommand> {
+  form: UseFormReturn<T>
+}
+
+const MultiplePosts = <T extends EmissionFactorCommand>({ form }: Props<T>) => {
+  const t = useTranslations('emissionFactors.create')
+  const tPost = useTranslations('emissionFactors.post')
+  const tGlossary = useTranslations('emissionFactors.create.glossary')
+
+  const control = form.control as Control<EmissionFactorCommand>
+  const setValue = form.setValue as UseFormSetValue<EmissionFactorCommand>
+
+  const [posts, setPosts] = useState<PostObject>({})
+  const [glossary, setGlossary] = useState('')
+
+  // check if post is in the list already to avoid issues
+  const postSelection: Post[] = useMemo(
+    () =>
+      Object.keys(Post)
+        .sort((a, b) => tPost(a).localeCompare(tPost(b)))
+        .filter((p) => !Object.keys(posts).includes(p)) as Post[],
+    [tPost, posts],
+  )
+
+  useEffect(() => {
+    const postObj = (form.getValues('subPosts' as FieldPath<T>) as PostObject) || {}
+    setPosts(postObj)
+  }, [form])
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    const currentSubPosts = { ...posts, [selectedPost]: [] }
+    setPosts(currentSubPosts)
+    setValue('subPosts', currentSubPosts)
+  }
+
+  const handleChange = (posts: PostObject) => {
+    setPosts(posts)
+  }
+
+  return (
+    <div className="flex-col">
+      {/* Real posts/Subpost selection */}
+      {Object.keys(posts).map((postKey) => (
+        <Box key={postKey} sx={{ mb: 2 }}>
+          <Posts
+            postOptions={postSelection}
+            onChange={handleChange}
+            form={form}
+            post={postKey as Post}
+            subPosts={posts[postKey as Post]}
+          />
+        </Box>
+      ))}
+
+      {/* Adding post logic from a select */}
+      <FormControl sx={{ width: '40%' }} error={Object.keys(posts).length === 0}>
+        <Select
+          name={'post'}
+          onChange={handleSelectPost}
+          data-testid="emission-factor-post"
+          label={t('posts')}
+          fullWidth
+          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
+          iconPosition="after"
+        >
+          {postSelection.map((post) => (
+            <MenuItem key={post} value={post}>
+              {tPost(post)}
+            </MenuItem>
+          ))}
+        </Select>
+      </FormControl>
+
+      {/* display error message */}
path: src/components/emissionFactor/Form/MultiplePosts.tsx
user: login: RomainCrevecoeur
body: On essaie d'éviter au maximum les commentaires - surtout côté HTML -
 (qui peuvent être amenés à ne plus être représentatif du code)
Et on essaie au maximum d'avoir du code qui s'explique par lui même
created_at: 2025-03-04T14:02:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,55 +42,88 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    if (post) {
+      // if post is already selected, reset subPosts
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+    onChange(currentSubPosts)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+    onChange(currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+    onChange(newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box sx={{ display: 'flex', w: '100%', gap: 2, alignItems: 'end' }}>
path: src/components/emissionFactor/Form/Posts.tsx
user: login: RomainCrevecoeur
body: Tu peux utiliser la classe globale align-end
created_at: 2025-03-04T14:08:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,55 +42,88 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    if (post) {
+      // if post is already selected, reset subPosts
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+    onChange(currentSubPosts)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+    onChange(currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+    onChange(newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box sx={{ display: 'flex', w: '100%', gap: 2, alignItems: 'end' }}>
path: src/components/emissionFactor/Form/Posts.tsx
user: login: fortinlouis
body: et la classe globale w100
created_at: 2025-03-04T14:15:09Z
author_association: COLLABORATOR
diff_hunk: @@ -33,55 +42,88 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    if (post) {
+      // if post is already selected, reset subPosts
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+    onChange(currentSubPosts)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+    onChange(currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+    onChange(newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box sx={{ display: 'flex', w: '100%', gap: 2, alignItems: 'end' }}>
+      <FormControl sx={{ width: '40%' }}>
         <Select
-          name="Post"
-          data-testid="emission-factor-post"
+          name="post"
           labelId="post-select-label"
           value={post || ''}
-          onChange={(event) => {
-            // @ts-expect-error: Force undefined to trigger error if not filled
-            setValue('subPost', undefined)
-            setPost(event.target.value as Post)
-          }}
+          onChange={handleSelectPost}
           label={t('post')}
-          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
-          iconPosition="after"
         >
           {posts.map((post) => (
-            <MenuItem key={post} value={post}>
+            // check if post is in the list already to avoid issues
+            <MenuItem disabled={!postOptions.includes(post as Post)} key={post} value={post}>
               {tPost(post)}
             </MenuItem>
           ))}
         </Select>
       </FormControl>
-      <FormSelect
-        data-testid="emission-factor-subPost"
-        control={control}
-        translation={t}
-        label={t('subPost')}
-        name="subPost"
+      <FormControl sx={{ width: '50%' }} error={selectedSubPosts?.length === 0}>
+        <MultiSelect
+          name="subPosts"
+          data-testid="emission-factor-subPost"
+          labelId="post-select-label"
+          value={selectedSubPosts || []}
+          onChange={handleSelectSubPost}
+          label={t('subPost')}
+          options={translatedSubPosts}
+          placeholder="placeholdertest"
+          translation={tPost}
+        />
+      </FormControl>
+      <Button
+        sx={{ flex: 1, minHeight: 'fit-content', height: '3.5rem' }}
path: src/components/emissionFactor/Form/Posts.tsx
user: login: RomainCrevecoeur
body: Pareil si tu peux utiliser des classes ici
created_at: 2025-03-04T14:17:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,55 +42,88 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    if (post) {
+      // if post is already selected, reset subPosts
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+    onChange(currentSubPosts)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+    onChange(currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+    onChange(newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box sx={{ display: 'flex', w: '100%', gap: 2, alignItems: 'end' }}>
+      <FormControl sx={{ width: '40%' }}>
         <Select
-          name="Post"
-          data-testid="emission-factor-post"
+          name="post"
           labelId="post-select-label"
           value={post || ''}
-          onChange={(event) => {
-            // @ts-expect-error: Force undefined to trigger error if not filled
-            setValue('subPost', undefined)
-            setPost(event.target.value as Post)
-          }}
+          onChange={handleSelectPost}
           label={t('post')}
-          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
-          iconPosition="after"
         >
           {posts.map((post) => (
-            <MenuItem key={post} value={post}>
+            // check if post is in the list already to avoid issues
+            <MenuItem disabled={!postOptions.includes(post as Post)} key={post} value={post}>
               {tPost(post)}
             </MenuItem>
           ))}
         </Select>
       </FormControl>
-      <FormSelect
-        data-testid="emission-factor-subPost"
-        control={control}
-        translation={t}
-        label={t('subPost')}
-        name="subPost"
+      <FormControl sx={{ width: '50%' }} error={selectedSubPosts?.length === 0}>
+        <MultiSelect
+          name="subPosts"
+          data-testid="emission-factor-subPost"
+          labelId="post-select-label"
+          value={selectedSubPosts || []}
+          onChange={handleSelectSubPost}
+          label={t('subPost')}
+          options={translatedSubPosts}
+          placeholder="placeholdertest"
+          translation={tPost}
+        />
+      </FormControl>
+      <Button
+        sx={{ flex: 1, minHeight: 'fit-content', height: '3.5rem' }}
path: src/components/emissionFactor/Form/Posts.tsx
user: login: fortinlouis
body: flex: 1 c'est `grow`
created_at: 2025-03-04T14:48:52Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,7 @@ describe('Create emission factor', () => {
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
 
-    cy.getByTestId('emission-factor-valid-button').click()
+    cy.getByTestId('emission-factor-valid-button').click({ force: true })
path: src/tests/end-to-end/app/create-emission-factor.cy.ts
user: login: fortinlouis
body: pourquoi ce force: true ?
created_at: 2025-03-04T15:57:22Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,7 @@ describe('Create emission factor', () => {
     cy.getByTestId('emission-factor-subPost').click()
     cy.get('[data-value="Electricite"]').click()
 
-    cy.getByTestId('emission-factor-valid-button').click()
+    cy.getByTestId('emission-factor-valid-button').click({ force: true })
path: src/tests/end-to-end/app/create-emission-factor.cy.ts
user: login: jilink
body: > pourquoi ce force: true ?

J'avais une erreur qui disait que le bouton submit n'apparaissait pas, en cherchant un peu j'ai vu qu'il pouvait être caché par le multiselect ou bien il est trop bas dans l'écran ... En tout cas ça a réglé le soucis 🤔
created_at: 2025-03-04T16:06:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,104 @@
+import HelpIcon from '@/components/base/HelpIcon'
+import { Select } from '@/components/base/Select'
+import GlossaryModal from '@/components/modals/GlossaryModal'
+import { Post, PostObject } from '@/services/posts'
+import { EmissionFactorCommand } from '@/services/serverFunctions/emissionFactor.command'
+import { Box, FormControl, FormHelperText, MenuItem, SelectChangeEvent } from '@mui/material'
+import { useTranslations } from 'next-intl'
+import { useEffect, useMemo, useState } from 'react'
+import { Control, Controller, FieldPath, UseFormReturn, UseFormSetValue } from 'react-hook-form'
+import Posts from './Posts'
+import styles from './Posts.module.css'
+
+interface Props<T extends EmissionFactorCommand> {
+  form: UseFormReturn<T>
+}
+
+const MultiplePosts = <T extends EmissionFactorCommand>({ form }: Props<T>) => {
+  const t = useTranslations('emissionFactors.create')
+  const tPost = useTranslations('emissionFactors.post')
+  const tGlossary = useTranslations('emissionFactors.create.glossary')
+
+  const control = form.control as Control<EmissionFactorCommand>
+  const setValue = form.setValue as UseFormSetValue<EmissionFactorCommand>
+
+  const [posts, setPosts] = useState<PostObject>({})
+  const [glossary, setGlossary] = useState('')
+
+  // check if post is in the list already to avoid issues
+  const postSelection: Post[] = useMemo(
+    () =>
+      Object.keys(Post)
+        .sort((a, b) => tPost(a).localeCompare(tPost(b)))
+        .filter((p) => !Object.keys(posts).includes(p)) as Post[],
+    [tPost, posts],
+  )
+
+  useEffect(() => {
+    const postObj = (form.getValues('subPosts' as FieldPath<T>) as PostObject) || {}
+    setPosts(postObj)
+  }, [form])
path: src/components/emissionFactor/Form/MultiplePosts.tsx
user: login: fortinlouis
body: Tu peux utiliser un form.watch('subPosts') ? Ça évite de rentrer dedans quand on change un autre post, et c'est plus précis
created_at: 2025-03-05T09:47:22Z
author_association: COLLABORATOR
diff_hunk: @@ -33,55 +43,86 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
path: src/components/emissionFactor/Form/Posts.tsx
user: login: fortinlouis
body: tPost ne va pas changer. On a l'habitude de laisser les warning dans ce genre de cas
created_at: 2025-03-05T09:49:32Z
author_association: COLLABORATOR
diff_hunk: @@ -33,55 +43,86 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(
+    () => subPosts.map((subP) => ({ label: tPost(subP), value: subP })),
+    [subPosts, tPost],
+  )
+
+  const handleSelectPost = (event: SelectChangeEvent<unknown>) => {
+    const selectedPost = event.target.value as Post
+    setSelectedSubPosts([])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    if (post) {
+      delete currentSubPosts[post]
+    }
+    currentSubPosts[selectedPost] = []
+    setValue('subPosts', currentSubPosts)
+
+    setPost(selectedPost)
+    onChange(currentSubPosts)
+  }
+
+  const handleDelete = () => {
+    if (!post) {
+      return
+    }
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    delete currentSubPosts[post]
+    setValue('subPosts', currentSubPosts)
+    onChange(currentSubPosts)
+  }
+
+  const handleSelectSubPost = (subPostsArr: string[]) => {
+    if (!post) {
+      return
+    }
+    setSelectedSubPosts(subPostsArr as SubPost[])
+    const currentSubPosts: PostObject = (form.getValues('subPosts' as Path<T>) as PostObject) || {}
+    const newSubPosts = { ...currentSubPosts, [post]: subPostsArr }
+    setValue('subPosts', newSubPosts)
+    onChange(newSubPosts)
+  }
+
   return (
-    <>
-      <FormControl>
+    <Box className="w100 align-end justify-between">
+      <FormControl className={styles.selectForm}>
         <Select
-          name="Post"
-          data-testid="emission-factor-post"
+          name="post"
           labelId="post-select-label"
           value={post || ''}
-          onChange={(event) => {
-            // @ts-expect-error: Force undefined to trigger error if not filled
-            setValue('subPost', undefined)
-            setPost(event.target.value as Post)
-          }}
+          onChange={handleSelectPost}
           label={t('post')}
-          icon={<HelpIcon onClick={() => setGlossary('post')} label={tGlossary('title')} />}
-          iconPosition="after"
         >
           {posts.map((post) => (
-            <MenuItem key={post} value={post}>
+            <MenuItem disabled={!postOptions.includes(post as Post)} key={post} value={post}>
               {tPost(post)}
             </MenuItem>
           ))}
         </Select>
       </FormControl>
-      <FormSelect
-        data-testid="emission-factor-subPost"
-        control={control}
-        translation={t}
-        label={t('subPost')}
-        name="subPost"
+      <FormControl className={styles.multiSelectForm} error={selectedSubPosts?.length === 0}>
+        <MultiSelect
+          name="subPosts"
+          data-testid="emission-factor-subPost"
+          labelId="post-select-label"
path: src/components/emissionFactor/Form/Posts.tsx
user: login: fortinlouis
body: ```suggestion
          labelId="subpost-select-label"
```
created_at: 2025-03-05T09:51:14Z
author_association: COLLABORATOR
diff_hunk: @@ -52,10 +53,18 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
+  const getSubPostObject = () => {
path: src/components/emissionFactor/edit/Form.tsx
user: login: fortinlouis
body: ce serait plus clair avec un reduce non ?
created_at: 2025-03-05T09:55:17Z
author_association: COLLABORATOR
diff_hunk: @@ -52,10 +53,18 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
+  const getSubPostObject = () => {
path: src/components/emissionFactor/edit/Form.tsx
user: login: fortinlouis
body: Et du coup un objet (en useMemo)
created_at: 2025-03-05T09:56:29Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,9 @@
 import { SubPost } from '@prisma/client'
 
+export type PostObject = {
path: src/services/posts.ts
user: login: fortinlouis
body: C'est pas très clair ce type : ce ne serait pas mieux d'utiliser `Record<Post, SubPost[]>`
created_at: 2025-03-05T09:59:26Z
author_association: COLLABORATOR
diff_hunk: @@ -52,10 +53,18 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
+  const getSubPostObject = () => {
path: src/components/emissionFactor/edit/Form.tsx
user: login: jilink
body: Bien vu c'est bcp plus clair merci !
created_at: 2025-03-05T10:27:30Z
author_association: COLLABORATOR
diff_hunk: @@ -33,55 +41,83 @@ const Posts = <T extends EmissionFactorCommand>({ form, post: initalPost }: Prop
     [post, tPost],
   )
 
+  const translatedSubPosts = useMemo(() => subPosts.map((subP) => ({ label: tPost(subP), value: subP })), [subPosts])
path: src/components/emissionFactor/Form/Posts.tsx
user: login: fortinlouis
body: On a pris l'habitude dans les boucles d'utiliser le nom entier de l'item, en supprimant simplment le "s" du nom de la liste. Dans ton cas : "subPost"
```suggestion
  const translatedSubPosts = useMemo(() => subPosts.map((subPost) => ({ label: tPost(subPost), value: subPost })), [subPosts])
```
created_at: 2025-03-05T16:01:16Z
author_association: COLLABORATOR
diff_hunk: @@ -52,11 +54,19 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
-  }
+  const subPostObject = useMemo(() => {
+    return emissionFactor.subPosts.reduce<Record<Post, SubPost[]>>(
+      (acc, subPost) => {
+        const post = getPost(subPost)
+        if (post) {
+          acc[post] = acc[post] ?? []
+          acc[post].push(subPost)
+        }
+        return acc
+      },
+      {} as Record<Post, SubPost[]>,
path: src/components/emissionFactor/edit/Form.tsx
user: login: fortinlouis
body: tu as besoin de caster à l'initialisation ?
created_at: 2025-03-05T16:02:26Z
author_association: COLLABORATOR
diff_hunk: @@ -52,11 +54,19 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
-  }
+  const subPostObject = useMemo(() => {
+    return emissionFactor.subPosts.reduce<Record<Post, SubPost[]>>(
+      (acc, subPost) => {
+        const post = getPost(subPost)
+        if (post) {
+          acc[post] = acc[post] ?? []
+          acc[post].push(subPost)
+        }
+        return acc
+      },
+      {} as Record<Post, SubPost[]>,
path: src/components/emissionFactor/edit/Form.tsx
user: login: jilink
body: > tu as besoin de caster à l'initialisation ?

j'ai cette erreur :

Argument of type '{}' is not assignable to parameter of type 'Record<Post, SubPost[]>'.
  Type '{}' is missing the following properties from type 'Record<Post, SubPost[]>': Energies, AutresEmissionsNonEnergetiques, IntrantsBiensEtMatieres, IntrantsServices, and 6 more.ts(2345)
  
  
  Je peux la résoudre soit comme ça soit en utilisant Partial
  
  ` return emissionFactor.subPosts.reduce<Partial<Record<Post, SubPost[]>>>(`
  
  Dis moi ce que tu préfères ou si tu as une autre idée
created_at: 2025-03-10T08:05:33Z
author_association: COLLABORATOR
diff_hunk: @@ -52,11 +54,19 @@ const EditEmissionFactorForm = ({ emissionFactor }: Props) => {
   const [hasParts, setHasParts] = useState(!!(emissionFactor.emissionFactorParts.length > 0))
   const [partsCount, setPartsCount] = useState(emissionFactor.emissionFactorParts.length || 1)
 
-  const subPost = emissionFactor?.subPosts[0] || undefined
-  let post: Post | undefined = undefined
-  if (subPost) {
-    post = Object.keys(subPostsByPost).find((post) => subPostsByPost[post as Post].includes(subPost)) as Post
-  }
+  const subPostObject = useMemo(() => {
+    return emissionFactor.subPosts.reduce<Record<Post, SubPost[]>>(
+      (acc, subPost) => {
+        const post = getPost(subPost)
+        if (post) {
+          acc[post] = acc[post] ?? []
+          acc[post].push(subPost)
+        }
+        return acc
+      },
+      {} as Record<Post, SubPost[]>,
path: src/components/emissionFactor/edit/Form.tsx
user: login: fortinlouis
body: Si c'est nécessaire ça ne me dérange pas :)
created_at: 2025-03-10T08:11:47Z
author_association: COLLABORATOR

PR comments:
user: login: RomainCrevecoeur
created_at: 2025-03-04T10:31:11Z
body: J'ai fait  des tests visuels et ça me paraît pas mal
Hésite pas à passer ta PR en non draft et passer l'issue https://github.com/ABC-TransitionBasCarbone/bilan-carbone/issues/493 en relecture
![image](https://github.com/user-attachments/assets/afa949c5-c4fc-4d99-a22b-c0f46f0cc458)
![image](https://github.com/user-attachments/assets/139dcf3b-1617-4f9b-be4d-03a39da457d3)

Si tu as des tests KO c'est sans doute des flaky test si tu relance la CI normalement ça va corriger ces tests



author_association: CONTRIBUTOR
user: login: jilink
created_at: 2025-03-04T10:36:15Z
body: > J'ai fait des tests visuels et ça me paraît pas mal Hésite pas à passer ta PR en non draft et passer l'issue #493 en relecture ![image](https://private-user-images.githubusercontent.com/162474058/418959733-afa949c5-c4fc-4d99-a22b-c0f46f0cc458.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEwODQ2ODEsIm5iZiI6MTc0MTA4NDM4MSwicGF0aCI6Ii8xNjI0NzQwNTgvNDE4OTU5NzMzLWFmYTk0OWM1LWM0ZmMtNGQ5OS1hMjJiLWMwZjQ2ZjBjYzQ1OC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwMzA0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDMwNFQxMDMzMDFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT1kNGI5NzYzOTZjZDIzZjI3MDg2NzBhNzEzOGY1OTRkZjY5NDdjMzE1YjkxYzBiZmU4OTk2MmI0MGRjMjY0N2QwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.HSE0WA2dokhMnhuEnU19kDCUrQVAi89Dj92Up7-W9XE) ![image](https://private-user-images.githubusercontent.com/162474058/418960759-139dcf3b-1617-4f9b-be4d-03a39da457d3.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEwODQ2ODEsIm5iZiI6MTc0MTA4NDM4MSwicGF0aCI6Ii8xNjI0NzQwNTgvNDE4OTYwNzU5LTEzOWRjZjNiLTE2MTctNGY5Yi1iZTRkLTAzYTM5ZGE0NTdkMy5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwMzA0JTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDMwNFQxMDMzMDFaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT03OTBlZjI4NDQ5ZjEyZTMzOTgyZGQ2ZTJiNzEyODg3OThhYWEzYjdiMTNmZjJkOTlhNjY0NjQyYzE1MmJlNDllJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.Vspa1V_tz6vK3xBSojlkRC4kv8FgItnNr0IOd4JY0cA)
> 
> Si tu as des tests KO c'est sans doute des flaky test si tu relance la CI normalement ça va corriger ces tests

Thanks ! Ah justement j'étais en train de corriger les test cypress je devrais pas tarder à finir et push ensuite je passerais la PR en non draft 🔥
author_association: COLLABORATOR
user: login: RomainCrevecoeur
created_at: 2025-03-04T13:43:32Z
body: Justement les PR c'est pas toi qui t'assigne mais plutôt les autres devs
author_association: CONTRIBUTOR
user: login: jilink
created_at: 2025-03-04T13:45:46Z
body: > Justement les PR c'est pas toi qui t'assigne mais plutôt les autres devs

Ca marche j'avais cette habitude lors de missions précédentes mais c'est noté 👍
author_association: COLLABORATOR