betagouv/service-national-universel

View on GitHub
admin/src/components/selectStatus.jsx

Summary

Maintainability
C
1 day
Test Coverage
import React, { useEffect, useState } from "react";
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from "reactstrap";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { ModalConfirmation } from "@snu/ds/admin";
import { IoWarningOutline } from "react-icons/io5";

import api from "../services/api";

import {
  translate,
  translatePhase1,
  translatePhase2,
  YOUNG_STATUS,
  YOUNG_PHASE,
  YOUNG_STATUS_COLORS,
  /* isEndOfInscriptionManagement2021, ROLES, */ colors,
  SENDINBLUE_TEMPLATES,
  WITHRAWN_REASONS,
  ROLES,
} from "../utils";
import { toastr } from "react-redux-toastr";

import ModalCorrection from "./modals/ModalCorrection";
import ModalRefused from "./modals/ModalRefused";
import ModalWithdrawn from "./modals/ModalWithdrawn";
import Chevron from "./Chevron";
import ModalConfirm from "./modals/ModalConfirm";
import ModalConfirmMultiAction from "./modals/ModalConfirmMultiAction";
import { YOUNG_STATUS_PHASE2, isCle, translateInscriptionStatus } from "snu-lib";

const lookUpAuthorizedStatus = ({ status, role }) => {
  switch (status) {
    case YOUNG_STATUS.WAITING_VALIDATION:
      return [YOUNG_STATUS.VALIDATED, YOUNG_STATUS.REFUSED, YOUNG_STATUS.WAITING_CORRECTION, YOUNG_STATUS.WAITING_LIST, YOUNG_STATUS.WITHDRAWN];
    case YOUNG_STATUS.WAITING_CORRECTION:
      return [YOUNG_STATUS.VALIDATED, YOUNG_STATUS.REFUSED, YOUNG_STATUS.WAITING_VALIDATION, YOUNG_STATUS.WAITING_LIST, YOUNG_STATUS.WITHDRAWN];
    case YOUNG_STATUS.WAITING_LIST:
      return [YOUNG_STATUS.VALIDATED, YOUNG_STATUS.WITHDRAWN];
    case YOUNG_STATUS.WITHDRAWN:
      if (role === ROLES.ADMIN) return [YOUNG_STATUS.VALIDATED, YOUNG_STATUS.WAITING_LIST];
      else return [];
    case YOUNG_STATUS.VALIDATED:
      return [YOUNG_STATUS.WITHDRAWN];
    default:
      return [];
  }
};

export default function SelectStatus({ hit, options = Object.keys(YOUNG_STATUS), statusName = "status", phase = YOUNG_PHASE.INSCRIPTION, disabled, callback = () => {} }) {
  const [modal, setModal] = useState(null);
  const [young, setYoung] = useState(null);
  const user = useSelector((state) => state.Auth.user);
  const [modalConfirm, setModalConfirm] = useState({ isOpen: false, onConfirm: null });
  const [modalGoal, setModalGoal] = useState({ isOpen: false, onConfirm: null });
  const [isModalValidatePhase2Open, setModalValidatePhase2] = useState(false);
  const [newStatus, setNewStatus] = useState(null);

  const getInscriptionGoalReachedNormalized = async ({ department, cohort }) => {
    const { data, ok, code } = await api.get(`/inscription-goal/${encodeURIComponent(cohort)}/department/${encodeURIComponent(department)}`);
    if (!ok) {
      toastr.error("Oups, une erreur s'est produite", translate(code));
      return null;
    }
    return data;
  };

  useEffect(() => {
    (async () => {
      const id = hit && hit._id;
      if (!id) return setYoung(null);
      const { data } = await api.get(`/referent/young/${id}`);
      setYoung(data);
    })();
  }, [hit._id]);

  if (!young) return <i style={{ color: colors.darkPurple }}>Chargement...</i>;

  const handleClickStatus = async (status) => {
    if (statusName === "statusPhase2" && status === YOUNG_STATUS_PHASE2.VALIDATED) {
      setNewStatus(status);
      setModalValidatePhase2(true);
    } else {
      setModalConfirm({
        isOpen: true,
        onConfirm: async () => {
          if ([YOUNG_STATUS.WAITING_CORRECTION, YOUNG_STATUS.REFUSED, YOUNG_STATUS.WITHDRAWN].includes(status)) return setModal(status);
          if (status === YOUNG_STATUS.VALIDATED && phase === YOUNG_PHASE.INSCRIPTION) {
            const fillingRate = await getInscriptionGoalReachedNormalized({ department: young.department, cohort: young.cohort });
            if (fillingRate >= 1)
              return setModalGoal({
                isOpen: true,
                onConfirm: () => handleChangeStatus(YOUNG_STATUS.WAITING_LIST),
                confirmText: "Placer en liste complémentaire",
                onConfirm2: () => handleChangeStatus(YOUNG_STATUS.VALIDATED),
                confirmText2: "Valider quand même",
                title: "Jauge de candidats atteinte",
                message:
                  "Attention, vous avez atteint la jauge, merci de placer le candidat sur liste complémentaire ou de vous rapprocher de votre coordinateur régional avant de valider la candidature.",
              });
          }
          handleChangeStatus(status);
        },
        title: "Modification de statut",
        message: "Êtes-vous sûr(e) de vouloir modifier le statut de ce profil?\nUn email sera automatiquement envoyé à l'utilisateur.",
      });
    }
  };

  const handleChangeStatus = async (status, values) => {
    const prevStatus = young.status;
    if (status === "WITHDRAWN") {
      young.historic.push({
        phase,
        userName: `${user.firstName} ${user.lastName}`,
        userId: user._id,
        status,
        note: WITHRAWN_REASONS.find((r) => r.value === values?.withdrawnReason)?.label + " " + values?.withdrawnMessage,
      });
    } else young.historic.push({ phase, userName: `${user.firstName} ${user.lastName}`, userId: user._id, status, note: values?.note });
    young[statusName] = status;
    const now = new Date();
    young.lastStatusAt = now.toISOString();
    if (statusName === "statusPhase2") young.statusPhase2UpdatedAt = now.toISOString();
    if (statusName === "statusPhase2" && status === YOUNG_STATUS_PHASE2.VALIDATED) young.statusPhase2ValidatedAt = now.toISOString();
    if (statusName === "statusPhase3") young.statusPhase3UpdatedAt = now.toISOString();
    if (status === "WITHDRAWN" && (values?.withdrawnReason || values?.withdrawnMessage)) {
      young.withdrawnReason = values?.withdrawnReason;
      young.withdrawnMessage = values?.withdrawnMessage || "";
    }
    if (status === "WAITING_CORRECTION" && values?.note) young.inscriptionCorrectionMessage = values?.note;
    if (status === "REFUSED" && values?.note) young.inscriptionRefusedMessage = values?.note;
    if (status === YOUNG_STATUS.VALIDATED && phase === YOUNG_PHASE.INTEREST_MISSION) young.phase = YOUNG_PHASE.CONTINUE;
    try {
      // we decided to let the validated youngs in the INSCRIPTION phase
      // referents use the export and need ALL the youngs of the current year
      // we'll pass every youngs currently in INSCRIPTION in COHESION_STAY once the campaign is done (~20 april 2021)

      // if (status === YOUNG_STATUS.VALIDATED && young.phase === YOUNG_PHASE.INSCRIPTION) {
      //   young.phase = YOUNG_PHASE.COHESION_STAY;
      // }

      const { lastStatusAt, statusPhase2UpdatedAt, statusPhase3UpdatedAt, withdrawnMessage, phase, inscriptionCorrectionMessage, inscriptionRefusedMessage, withdrawnReason } =
        young;

      const shouldValidateStatusPhase2 = statusName === "statusPhase2" && young[statusName] === YOUNG_STATUS.VALIDATED;
      const shouldValidateStatusPhase3 = statusName === "statusPhase3" && young[statusName] === YOUNG_STATUS.VALIDATED;

      const {
        ok,
        code,
        data: newYoung,
      } = await api.put(`/referent/young/${young._id}`, {
        [statusName]: young[statusName],
        lastStatusAt,
        ...(statusName === "statusPhase2" && { statusPhase2UpdatedAt }),
        ...(shouldValidateStatusPhase2 && { statusPhase2ValidatedAt: statusPhase2UpdatedAt }),
        ...(statusName === "statusPhase3" && { statusPhase3UpdatedAt }),
        ...(shouldValidateStatusPhase3 && { statusPhase3ValidatedAt: statusPhase3UpdatedAt }),
        withdrawnMessage,
        phase,
        inscriptionCorrectionMessage,
        inscriptionRefusedMessage,
        withdrawnReason,
      });
      if (!ok) {
        setModalValidatePhase2(false);
        return toastr.error("Une erreur s'est produite :", translate(code));
      }

      const validationTemplate = isCle(young) ? SENDINBLUE_TEMPLATES.young.INSCRIPTION_VALIDATED_CLE : SENDINBLUE_TEMPLATES.young.INSCRIPTION_VALIDATED;

      if (status === YOUNG_STATUS.VALIDATED && phase === YOUNG_PHASE.INSCRIPTION) {
        if (prevStatus === "WITHDRAWN") await api.post(`/young/${young._id}/email/${SENDINBLUE_TEMPLATES.young.INSCRIPTION_REACTIVATED}`);
        else await api.post(`/young/${young._id}/email/${validationTemplate}`);
      }
      if (status === YOUNG_STATUS.WAITING_LIST) {
        await api.post(`/young/${young._id}/email/${SENDINBLUE_TEMPLATES.young.INSCRIPTION_WAITING_LIST}`);
      }
      setYoung(newYoung);
      setModalValidatePhase2(false);
      toastr.success("Mis à jour!");
      callback();
    } catch (e) {
      console.log(e);
      toastr.error("Oups, une erreur est survenue :", translate(e.code));
      setModalValidatePhase2(false);
    }
  };

  const translator = (status) => {
    if (statusName === "statusPhase1") {
      return translatePhase1(status);
    } else if (statusName === "statusPhase2") {
      return translatePhase2(status);
    } else if (statusName === "status") {
      return translateInscriptionStatus(status);
    } else {
      return translate(status);
    }
  };

  if (statusName === "status") {
    options = lookUpAuthorizedStatus({ status: young[statusName], role: user.role });
  }

  return (
    <>
      <ModalConfirm
        isOpen={modalConfirm?.isOpen}
        title={modalConfirm?.title}
        message={modalConfirm?.message}
        headerText="Confirmation"
        onCancel={() => setModalConfirm({ isOpen: false, onConfirm: null })}
        onConfirm={() => {
          modalConfirm?.onConfirm?.();
          setModalConfirm({ isOpen: false, onConfirm: null });
        }}
      />
      <ModalCorrection
        isOpen={modal === YOUNG_STATUS.WAITING_CORRECTION}
        value={young}
        onChange={() => setModal(false)}
        onSend={(note) => {
          handleChangeStatus(YOUNG_STATUS.WAITING_CORRECTION, { note });
          setModal(null);
        }}
      />
      <ModalRefused
        isOpen={modal === YOUNG_STATUS.REFUSED}
        value={young}
        onChange={() => setModal(false)}
        onSend={(note) => {
          handleChangeStatus(YOUNG_STATUS.REFUSED, { note });
          setModal(null);
        }}
      />
      <ModalWithdrawn
        young={young}
        isOpen={modal === YOUNG_STATUS.WITHDRAWN}
        title="Desistement du SNU"
        message="Précisez la raison du désistement"
        placeholder="Précisez en quelques mots les raisons du désistement du volontaire"
        onChange={() => setModal(false)}
        onConfirm={(values) => {
          handleChangeStatus(YOUNG_STATUS.WITHDRAWN, values);
          setModal(null);
        }}
      />
      <ModalConfirmMultiAction
        showHeaderIcon={true}
        showHeaderText={false}
        isOpen={modalGoal?.isOpen}
        title={modalGoal?.title}
        message={modalGoal?.message}
        confirmText={modalGoal?.confirmText}
        confirmText2={modalGoal?.confirmText2}
        onConfirm={() => {
          modalGoal?.onConfirm?.();
          setModalGoal({ isOpen: false, onConfirm: null });
          setModal(null);
        }}
        onConfirm2={() => {
          modalGoal?.onConfirm2?.();
          setModalGoal({ isOpen: false, onConfirm: null });
          setModal(null);
        }}
        onCancel={() => {
          setModalGoal({ isOpen: false, onConfirm: null });
          setModal(null);
        }}
      />
      <ModalConfirmation
        isOpen={isModalValidatePhase2Open}
        onClose={() => {
          setModalValidatePhase2(false);
        }}
        className="md:max-w-[700px]"
        icon={<IoWarningOutline className="text-amber-500" size={40} />}
        title="Attention, vous êtes sur le point de valider la phase 2 de ce volontaire"
        text="Cette action entraînera l'abandon des candidatures et des demandes d'équivalence encore en cours. Êtes-vous sûr de vouloir continuer ?"
        actions={[
          { title: "Annuler", isCancel: true },
          { title: "Confirmer", onClick: () => handleChangeStatus(newStatus) },
        ]}
      />

      <ActionBox color={YOUNG_STATUS_COLORS[young[statusName]]}>
        <UncontrolledDropdown setActiveFromChild>
          <DropdownToggle tag="button" disabled={disabled || !options.length}>
            {translator(young[statusName])}
            {!!options.length && !disabled && <Chevron color={YOUNG_STATUS_COLORS[young[statusName]]} />}
          </DropdownToggle>
          <DropdownMenu>
            {options
              .filter((e) => e !== young[statusName])
              .map((status) => {
                return (
                  <DropdownItem key={status} className="dropdown-item" onClick={() => handleClickStatus(status)}>
                    {translator(status)}
                  </DropdownItem>
                );
              })}
          </DropdownMenu>
        </UncontrolledDropdown>
        {/* <div>{JSON.stringify(young)}</div> */}
      </ActionBox>
    </>
  );
}

const ActionBox = styled.div`
  .dropdown-menu {
    max-width: 250px;
    min-width: 250px;
    a,
    div {
      white-space: nowrap;
      font-size: 14px;
      :hover {
        color: inherit;
      }
    }
  }
  button {
    ${({ color }) => `
      background-color: ${color}15;
      border: 1px solid ${color};
      color: ${color};
    `}
    display: inline-flex;
    flex: 1;
    justify-content: space-between;
    align-items: center;
    text-align: left;
    border-radius: 0.5rem;
    padding: 0 0 0 12px;
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    outline: 0;
    width: 100%;
    max-width: 250px;
    min-width: 250px;
    min-height: 34px;
    .edit-icon {
      height: 17px;
      margin-right: 10px;
      path {
        fill: ${({ color }) => `${color}`};
      }
    }
    .down-icon {
      margin-left: auto;
      padding: 7px 15px;
      margin-left: 15px;
      svg {
        height: 10px;
      }
      svg polygon {
        fill: ${({ color }) => `${color}`};
      }
    }
  }
  .dropdown-item {
    border-radius: 0;
    background-color: transparent;
    border: none;
    color: #767676;
    white-space: nowrap;
    font-size: 14px;
    padding: 5px 15px;
    font-weight: 400;
    :hover {
      background-color: #f3f3f3;
    }
  }
`;