Commit fcc84636 authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge branch '967-add-export-of-assignment-of-trainees' into 'main'

Add export of trainees in pdf

Closes #967

See merge request inject/frontend!897
parents e59c5269 e572ca5d
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ export const ExerciseCard: FC<ExerciseCardProps> = ({
  const { name, definition, config, states } = exercise
  const allowedStartInterval = exerciseAllowedStartInterval(exercise)
  const exactFormatSeconds = useExactSecondsFormatter({})

  return (
    <div className={exerciseClass}>
      <h3>{name}</h3>
+1 −1
Original line number Diff line number Diff line
@@ -8,9 +8,9 @@ import type { NavigateOptions } from '@tanstack/react-router'
import { useState, type FC } from 'react'
import ConfirmAlert from '../../../exercisepanel/ConfirmAlert'
import { DownloadLogsButton } from './DownloadLogsButton'
import { start, useLoadingState } from './mutations'
import { OnDemandExerciseButtons } from './OnDemandExerciseButtons'
import { SynchronousExerciseButtons } from './SynchronousExerciseButtons'
import { start, useLoadingState } from './mutations'

interface ManagingExerciseButtonsProps {
  exercise: ExerciseSimple
+3 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import AssignByTags from './AssignByTags'
import AssignEqually from './AssignEqually'
import { bodySection } from './classes'
import TeamComponent from './Team'
import { DownloadTraineesButton } from './TraineesButton/DownloadTraineesButton'
import UnassignAllExercise from './UnassignAllExercise'

const body = css`
@@ -36,6 +37,8 @@ const Teams: FC<TeamsProps> = ({ teams, exerciseId }) => {
            <AssignEqually exerciseId={exerciseId} teamCount={teams.length} />
            <Divider />
            <AssignByTags exerciseId={exerciseId} />
            <Divider />
            <DownloadTraineesButton exerciseId={exerciseId} />
          </ButtonGroup>
        }
      >
+76 −0
Original line number Diff line number Diff line
import type { ButtonProps } from '@blueprintjs/core'
import { Button } from '@blueprintjs/core'
import type { Team } from '@inject/graphql'
import { GetExercise, useClient } from '@inject/graphql'
import { useTranslationFrontend } from '@inject/locale'
import { pdf } from '@react-pdf/renderer'
import { useState, type FC } from 'react'
import { AssigneesPDF } from './TraineesPDF'

interface DownloadTraineesButtonProps {
  exerciseId: string
  buttonProps?: ButtonProps
}

//TODO: Add export also in .csv format, add option to choose between formats
export const DownloadTraineesButton: FC<DownloadTraineesButtonProps> = ({
  exerciseId,
}) => {
  const [isLoading, setIsLoading] = useState(false)
  const { t } = useTranslationFrontend()

  const client = useClient()

  const handleExportPDF = async () => {
    setIsLoading(true)
    try {
      const { data } = await client.query(
        GetExercise,
        { exerciseId },
        { requestPolicy: 'network-only' }
      )
      const teamsData = (data?.exerciseId?.teams ?? []) as Team[]
      const exerciseName = data?.exerciseId?.name as string
      const teamUserTriplets = teamsData
        .flatMap((team: Team) =>
          (team.users ?? []).map(user => ({
            teamName: team.name ?? '',
            firstName: user.firstName ?? '',
            lastName: user.lastName ?? '',
          }))
        )
        .sort((a, b) => {
          const lastNameCompare = a.lastName.localeCompare(b.lastName)
          if (lastNameCompare !== 0) {
            return lastNameCompare
          }
          return a.firstName.localeCompare(b.firstName)
        })

      const blob = await pdf(
        <AssigneesPDF triplets={teamUserTriplets} exerciseName={exerciseName} />
      ).toBlob()
      const url = URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.setAttribute('href', url)
      link.setAttribute('download', `${exerciseName}-assignees.pdf`)
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <Button
      icon='download'
      loading={isLoading}
      onClick={handleExportPDF}
      alignText='left'
      fill
      minimal
      text={t('sidebar.downloadAssignees')}
    />
  )
}
+170 −0
Original line number Diff line number Diff line
import { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer'
import { type FC } from 'react'

const styles = StyleSheet.create({
  page: {
    padding: 24,
    fontSize: 10,
    fontFamily: 'Courier',
  },
  header: {
    textAlign: 'center',
    fontSize: 14,
    marginBottom: 12,
  },
  pageHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'flex-start',
    marginBottom: 12,
  },
  date: {
    fontSize: 10,
  },
  columns: {
    flexDirection: 'row',
    gap: 12,
  },
  column: {
    width: '50%',
  },
  table: {
    borderWidth: 1,
    borderColor: '#000',
  },
  row: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#000',
  },
  cell: {
    padding: 4,
    borderRightWidth: 1,
    borderRightColor: '#000',
  },
  teamCell: {
    width: '30%',
  },
  lastNameCell: {
    width: '35%',
  },
  firstNameCell: {
    width: '35%',
    borderRightWidth: 0,
  },
  headerText: {
    fontWeight: 'bold',
  },
  empty: {
    textAlign: 'center',
    marginTop: 24,
  },
})

export interface TeamUserTriplet {
  teamName: string
  firstName: string
  lastName: string
}

interface AssigneesPDFProps {
  triplets: TeamUserTriplet[]
  exerciseName: string
}

const chunk = <T,>(items: T[], size: number) => {
  const result: T[][] = []
  for (let i = 0; i < items.length; i += size) {
    result.push(items.slice(i, i + size))
  }
  return result
}

const TableHeader = () => (
  <View style={styles.row}>
    <Text style={[styles.cell, styles.teamCell, styles.headerText]}>team</Text>
    <Text style={[styles.cell, styles.lastNameCell, styles.headerText]}>
      last_name
    </Text>
    <Text style={[styles.cell, styles.firstNameCell, styles.headerText]}>
      first_name
    </Text>
  </View>
)

const TableBody: FC<{ triplets: TeamUserTriplet[] }> = ({ triplets }) => (
  <>
    {triplets.map((triplet, index) => (
      <View key={`${triplet.teamName}-${index}`} style={styles.row}>
        <Text style={[styles.cell, styles.teamCell]}>{triplet.teamName}</Text>
        <Text style={[styles.cell, styles.lastNameCell]}>
          {triplet.lastName}
        </Text>
        <Text style={[styles.cell, styles.firstNameCell]}>
          {triplet.firstName}
        </Text>
      </View>
    ))}
  </>
)

export const AssigneesPDF: FC<AssigneesPDFProps> = ({
  triplets,
  exerciseName,
}) => {
  if (triplets.length === 0) {
    return (
      <Document>
        <Page size='A4' orientation='portrait' style={styles.page}>
          <View style={styles.pageHeader}>
            <Text />
            <Text style={styles.header}>{exerciseName}</Text>
            <Text />
          </View>
          <Text style={styles.empty}>There are no trainees.</Text>
        </Page>
      </Document>
    )
  }

  const pages = chunk(triplets, 50)

  return (
    <Document>
      {pages.map((pageTriplets, pageIndex) => {
        const left = pageTriplets.slice(0, 25)
        const right = pageTriplets.slice(25, 50)
        return (
          <Page
            key={`page-${pageIndex}`}
            size='A4'
            orientation='portrait'
            style={styles.page}
          >
            <View style={styles.pageHeader}>
              <Text />
              <Text style={styles.header}>{exerciseName}</Text>
              <Text />
            </View>
            <View style={styles.columns}>
              <View style={styles.column}>
                <View style={styles.table}>
                  <TableHeader />
                  <TableBody triplets={left} />
                </View>
              </View>
              {right.length > 1 ? (
                <View style={styles.column}>
                  <View style={styles.table}>
                    <TableHeader />
                    <TableBody triplets={right} />
                  </View>
                </View>
              ) : undefined}
            </View>
          </Page>
        )
      })}
    </Document>
  )
}
Loading