Coursemology/coursemology2

View on GitHub
client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { FC, useEffect, useState } from 'react';
import {
  defineMessages,
  FormattedMessage,
  injectIntl,
  WrappedComponentProps,
} from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { Button, Checkbox, Grid, Tooltip, Typography } from '@mui/material';
import { blue, green, red } from '@mui/material/colors';
import equal from 'fast-deep-equal';
import { TableColumns, TableOptions } from 'types/components/DataTable';
import {
  AchievementCourseUserEntity,
  AchievementEntity,
} from 'types/course/achievements';

import { getAchievementBadgeUrl } from 'course/helper/achievements';
import ConfirmationDialog from 'lib/components/core/dialogs/ConfirmationDialog';
import DataTable from 'lib/components/core/layouts/DataTable';
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
import Note from 'lib/components/core/Note';
import { getAchievementURL } from 'lib/helpers/url-builders';
import { getCourseId } from 'lib/helpers/url-helpers';
import { useAppDispatch } from 'lib/hooks/store';
import toast from 'lib/hooks/toast';
import { formatShortDateTime } from 'lib/moment';

import { awardAchievement } from '../../operations';

import AchievementAwardSummary from './AchievementAwardSummary';

interface Props extends WrappedComponentProps {
  achievement: AchievementEntity;
  isLoading: boolean;
  handleClose: (skipDialog: boolean) => void;
  setIsDirty?: (value: boolean) => void;
}

const styles = {
  badge: {
    maxHeight: 75,
    maxWidth: 75,
    marginRight: 16,
  },
  checkbox: {
    margin: '0px 12px 0px 0px',
    padding: 0,
  },
  courseUserImage: {
    maxHeight: 75,
    maxWidth: 75,
  },
  description: {
    maxWidth: 1200,
  },
  textField: {
    width: '100%',
    marginBottom: '0.5rem',
  },
};

const translations = defineMessages({
  awardSuccess: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.awardSuccess',
    defaultMessage: 'Achievement was successfully awarded and/or revoked.',
  },
  awardFailure: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.awardFailure',
    defaultMessage: 'Failed to award achievement.',
  },
  confirmationQuestion: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.confirmationQuestion',
    defaultMessage: 'Are you sure you wish to make the following changes?',
  },
  note: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.note',
    defaultMessage:
      'If an Achievement has conditions associated with it, \
      Coursemology will automatically award achievements when the student meets those conditions. ',
  },
  noUser: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.noUser',
    defaultMessage: 'There is no available user to be awarded.',
  },
  obtainedAchievement: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.obtainedAchievement',
    defaultMessage: 'Obtained Achievement',
  },
  saveChanges: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.saveChanges',
    defaultMessage: 'Save Changes',
  },
  resetChanges: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.resetChanges',
    defaultMessage: 'Reset Changes',
  },
  cancel: {
    id: 'course.achievement.AchievementAward.AchievementAwardManager.cancel',
    defaultMessage: 'Cancel',
  },
});

const getObtainedUserIds = (
  courseUsers: AchievementCourseUserEntity[],
): number[] =>
  courseUsers.filter((cu) => cu.obtainedAt !== null).map((cu) => cu.id);

const AchievementAwardManager: FC<Props> = (props) => {
  const { achievement, isLoading, handleClose, intl, setIsDirty } = props;
  const achievementUsers = achievement.achievementUsers;

  const [openConfirmation, setOpenConfirmation] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const obtainedUserIds = getObtainedUserIds(achievementUsers);
  const [selectedUserIds, setSelectedUserIds] = useState(
    new Set(obtainedUserIds),
  );

  const dispatch = useAppDispatch();
  const navigate = useNavigate();

  const isPristine = equal(Array.from(selectedUserIds), obtainedUserIds);

  useEffect(() => {
    if (!isLoading && achievementUsers && setIsDirty) {
      if (!isPristine) {
        setIsDirty(true);
      } else {
        setIsDirty(false);
      }
    }
  }, [dispatch, isPristine, isLoading, achievementUsers]);

  if (isLoading) {
    return <LoadingIndicator />;
  }

  if (!achievementUsers || achievementUsers.length === 0) {
    return <Note message={<FormattedMessage {...translations.noUser} />} />;
  }

  const onSubmit = (
    achievementId: number,
    courseUserIds: number[],
  ): Promise<void> =>
    dispatch(awardAchievement(achievementId, courseUserIds))
      .then(() => {
        toast.success(intl.formatMessage(translations.awardSuccess));
        setTimeout(() => {
          navigate(getAchievementURL(getCourseId(), achievementId));
        }, 100);
      })
      .catch(() => {
        toast.error(intl.formatMessage(translations.awardFailure));
      });

  const options: TableOptions = {
    customToolbar: () => (
      <>
        <Button color="secondary" onClick={(): void => handleClose(false)}>
          <FormattedMessage {...translations.cancel} />
        </Button>
        <Button
          disabled={isPristine}
          onClick={(): void => setSelectedUserIds(new Set(obtainedUserIds))}
        >
          <FormattedMessage {...translations.resetChanges} />
        </Button>
        <Button
          disabled={isPristine}
          onClick={(): void => setOpenConfirmation(true)}
        >
          <FormattedMessage {...translations.saveChanges} />
        </Button>
      </>
    ),
    download: false,
    filter: false,
    jumpToPage: true,
    pagination: false,
    print: false,
    selectableRows: 'none',
    setRowProps: (_row, dataIndex, _rowIndex) => {
      const obtainedAchievement =
        achievementUsers[dataIndex].obtainedAt !== null;
      const awardedAchievement = selectedUserIds.has(
        achievementUsers[dataIndex].id,
      );
      let backgroundColor: unknown = null;
      if (!obtainedAchievement && awardedAchievement) {
        backgroundColor = green[100];
      } else if (obtainedAchievement && !awardedAchievement) {
        backgroundColor = red[100];
      } else if (obtainedAchievement) {
        backgroundColor = blue[100];
      }
      return { style: { background: backgroundColor } };
    },
    viewColumns: false,
  };

  const columnHeadLabelAchievement = intl.formatMessage(
    translations.obtainedAchievement,
  );

  const columns: TableColumns[] = [
    {
      name: 'name',
      label: 'Name',
      options: {
        filter: false,
      },
    },
    {
      name: 'phantom',
      label: 'User Type',
      options: {
        search: false,
        customBodyRenderLite: (dataIndex): string => {
          const isPhantom = achievementUsers[dataIndex].phantom;
          if (isPhantom) {
            return 'Phantom Student';
          }
          return 'Normal Student';
        },
      },
    },
    {
      name: 'obtainedAt',
      label: 'Obtained At',
      options: {
        filter: false,
        search: false,
        customBodyRenderLite: (dataIndex): string => {
          const achievementObtainedDate =
            achievementUsers[dataIndex].obtainedAt;
          return formatShortDateTime(achievementObtainedDate);
        },
      },
    },
    {
      name: 'id',
      label: 'Obtained Achievement',
      options: {
        filter: false,
        search: false,
        sort: false,
        customBodyRenderLite: (dataIndex): JSX.Element => {
          const userId = achievementUsers[dataIndex].id;
          const isChecked = selectedUserIds.has(userId);
          return (
            <Checkbox
              key={`checkbox_${userId}`}
              checked={isChecked}
              id={`checkbox_${userId}`}
              onChange={(_event, checked): void => {
                if (checked) {
                  setSelectedUserIds((prev) => new Set(prev.add(userId)));
                } else {
                  setSelectedUserIds(
                    (prev) => new Set([...prev].filter((x) => x !== userId)),
                  );
                }
              }}
              style={styles.checkbox}
            />
          );
        },
        customHeadLabelRender: (): JSX.Element => (
          <div style={{ display: 'flex', alignItems: 'end' }}>
            <Checkbox
              defaultChecked={false}
              onChange={(_event, checked): void => {
                if (checked) {
                  setSelectedUserIds(
                    new Set(achievementUsers.map((cu) => cu.id)),
                  );
                } else {
                  setSelectedUserIds(new Set());
                }
              }}
              style={styles.checkbox}
            />
            {columnHeadLabelAchievement}
          </div>
        ),
      },
    },
  ];

  return (
    <>
      <Grid container>
        <Grid
          alignItems="center"
          display="flex"
          item
          justifyContent="center"
          style={{ marginBottom: 8 }}
          xs={12}
        >
          <Tooltip
            title={
              achievement.achievementStatus ? achievement.achievementStatus : ''
            }
          >
            <img
              alt={achievement.badge.name}
              src={getAchievementBadgeUrl(
                achievement.badge.url,
                achievement.permissions.canDisplayBadge,
              )}
              style={styles.badge}
            />
          </Tooltip>
          <div style={styles.description}>
            <Typography
              dangerouslySetInnerHTML={{ __html: achievement.description }}
              style={{ whiteSpace: 'normal' }}
              variant="body2"
            />
          </div>
        </Grid>
        <Grid item xs={12}>
          <DataTable
            columns={columns}
            data={achievementUsers}
            includeRowNumber
            options={options}
          />
        </Grid>
      </Grid>
      {openConfirmation && (
        <ConfirmationDialog
          confirmButtonText={<FormattedMessage {...translations.saveChanges} />}
          disableCancelButton={isSubmitting}
          disableConfirmButton={isSubmitting}
          message={
            <>
              <p>
                <FormattedMessage {...translations.confirmationQuestion} />
              </p>
              <AchievementAwardSummary
                achievementUsers={achievementUsers}
                initialObtainedUserIds={obtainedUserIds}
                selectedUserIds={selectedUserIds}
              />
            </>
          }
          onCancel={(): void => setOpenConfirmation(false)}
          onConfirm={(): void => {
            setIsSubmitting(true);
            onSubmit(achievement.id, Array.from(selectedUserIds))
              .then(() => handleClose(true))
              .catch(() => {
                setIsSubmitting(false);
                setOpenConfirmation(false);
              });
          }}
          open={openConfirmation}
        />
      )}
    </>
  );
};

export default injectIntl(AchievementAwardManager);