Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React, { useState } from 'react';
import Button from '@material-ui/core/Button';
import Checkbox from '@material-ui/core/Checkbox';
import Grid from '@material-ui/core/Grid';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import InputLabel from '@material-ui/core/InputLabel';

import { downloadEmptyScorecardsForPersons } from '../../../../logic/documents/scorecards';
import {
activityCodeToName,
competitorsRegisteredForAnEventWithoutGroups,
} from '../../../../logic/activities';
import languageInfo from '../../../../logic/translations';
import { Avatar, ListItemAvatar } from '@material-ui/core';

const OTSScorecards = ({ wcif }) => {
const missingScorecards = competitorsRegisteredForAnEventWithoutGroups(wcif);
const [selectedCompetitors, setSelectedCompetitors] = useState(
missingScorecards.map(c => c.person.wcaUserId)
);

const handleCompetitorClick = competitor => {
const id = competitor.person.wcaUserId;
setSelectedCompetitors(
selectedCompetitors.includes(id)
? selectedCompetitors.filter(c => c !== id)
: [...selectedCompetitors, id]
);
};

const competitorSelected = competitor =>
selectedCompetitors.includes(competitor.person.wcaUserId);

const isSelectionEmpty = selectedCompetitors.length === 0;

const [language, setLanguage] = useState('en');

return (
<Paper style={{ padding: 16 }}>
<Grid container>
<Grid item xs={6}>
<Typography variant="subtitle1">Select competitors</Typography>
<List style={{ width: 400 }}>
{missingScorecards.map(scorecards => (
<ListItem
key={scorecards.person.wcaUserId}
button
onClick={() => handleCompetitorClick(scorecards)}
style={
missingScorecards.includes(scorecards) ? {} : { opacity: 0.5 }
}
>
<ListItemAvatar>
<Avatar
alt={scorecards.person.name}
src={scorecards.person.avatar.thumbUrl}
/>
</ListItemAvatar>
<ListItemText
primary={scorecards.person.name}
secondary={scorecards.eventIds
.map(eventId => activityCodeToName(`${eventId}-r1`))
.join(', ')}
/>
<Checkbox
checked={competitorSelected(scorecards)}
tabIndex={-1}
disableRipple
style={{ padding: 0 }}
/>
</ListItem>
))}
</List>
</Grid>
</Grid>
<Grid container spacing={2} style={{ marginTop: 16, marginBottom: 16 }}>
<Grid item xs={4}>
<FormControl variant="outlined" fullWidth>
<InputLabel>Scorecards language</InputLabel>
<Select
value={language}
onChange={e => setLanguage(e.target.value)}
label="Scorecards language"
>
{languageInfo.map(({ code, originalName, englishName }) => (
<MenuItem key={code} value={code}>
{originalName === englishName
? originalName
: `${originalName} (${englishName})`}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container spacing={1}>
<Grid item>
<Button
onClick={() =>
downloadEmptyScorecardsForPersons(
wcif,
selectedCompetitors,
language
)
}
disabled={isSelectionEmpty}
>
Scorecards
</Button>
</Grid>
</Grid>
</Paper>
);
};

export default OTSScorecards;
3 changes: 3 additions & 0 deletions src/components/Competition/PrintingManager/PrintingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
roundsMissingAssignments,
activityCodeToName,
} from '../../../logic/activities';
import OTSScorecards from './OTSScorecards/OTSScorecards';

const PrintingManager = ({ wcif }) => {
const [tabValue, setTabValue] = useState(0);
Expand Down Expand Up @@ -49,11 +50,13 @@ const PrintingManager = ({ wcif }) => {
<Tabs value={tabValue} onChange={(event, value) => setTabValue(value)}>
<Tab label="Scorecards" />
<Tab label="Competitor cards" />
<Tab label="OTS scorecards" />
</Tabs>
</Grid>
<Grid item xs={12}>
{tabValue === 0 && <Scorecards wcif={wcif} />}
{tabValue === 1 && <CompetitorCards wcif={wcif} />}
{tabValue === 2 && <OTSScorecards wcif={wcif} />}
</Grid>
<Grid item>
<Button
Expand Down
25 changes: 25 additions & 0 deletions src/logic/activities.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,31 @@ export const roundsMissingScorecards = wcif =>
.filter(round => groupActivitiesAssigned(wcif, round.id))
.filter(round => parseActivityCode(round.id).eventId !== '333fm');

export const competitorsRegisteredForAnEventWithoutGroups = wcif => {
let persons = {};
wcif.persons.forEach(person => {
if (!person.registration || person.registration?.status !== 'accepted') {
return;
}
person.registration.eventIds.forEach(eventId => {
const hasCompetitorAssignment = person.assignments.some(
a =>
a.assignmentCode === 'competitor' &&
activityById(wcif, a.activityId).activityCode.startsWith(
`${eventId}-r1`
)
);
if (!hasCompetitorAssignment) {
if (!persons[person.wcaUserId]) {
persons[person.wcaUserId] = { person, eventIds: [] };
}
persons[person.wcaUserId].eventIds.push(eventId);
}
});
});
return Object.values(persons);
};

export const allGroupsCreated = wcif =>
wcif.events.every(event =>
event.rounds.every(
Expand Down
142 changes: 117 additions & 25 deletions src/logic/documents/scorecards.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
groupActivitiesByRound,
hasDistributedAttempts,
roomByActivity,
competitorsRegisteredForAnEventWithoutGroups,
} from '../activities';
import { eventNameById } from '../events';
import { cutoffToString, timeLimitToString } from '../formatters';
Expand Down Expand Up @@ -63,6 +64,111 @@ export const downloadScorecards = (wcif, rounds, rooms, language) => {
});
};

export const downloadEmptyScorecardsForPersons = (
wcif,
selectedPersons,
language = 'en'
) => {
const personsWithoutGroups = competitorsRegisteredForAnEventWithoutGroups(
wcif
);
const filteredPersonsWithoutGroups = personsWithoutGroups.filter(
({ person }) => selectedPersons.includes(person.wcaUserId)
);
const { scorecardsBackgroundUrl, scorecardPaperSize } = getExtensionData(
'CompetitionConfig',
wcif
);

getImageDataUrl(scorecardsBackgroundUrl).then(imageData => {
const pdfDefinition = scorecardsPdfDefinition(
emptyScorecardsForPersons(wcif, filteredPersonsWithoutGroups, language),
imageData,
scorecardPaperSize
);
pdfMake
.createPdf(pdfDefinition)
.download(`${wcif.id}-missing-scorecards.pdf`);
});
};

const prepareScorecardsPages = (cards, scorecardsPerPage, scorecardOrder) => {
const scorecardsOnLastPage = cards.length % scorecardsPerPage;

if (scorecardsOnLastPage !== 0 && scorecardOrder !== 'stacked') {
cards = cards.concat(
times(scorecardsPerPage - scorecardsOnLastPage, () => ({}))
);
}

if (scorecardOrder === 'stacked') {
if (scorecardsOnLastPage !== 0) {
cards = cards.concat(
times(scorecardsPerPage - scorecardsOnLastPage, () => ({}))
);
}
cards = cards
.map((card, idx) => ({ overallNumber: idx, card }))
.sort((a, b) => {
const sectionA = a.overallNumber % (cards.length / scorecardsPerPage);
const sectionB = b.overallNumber % (cards.length / scorecardsPerPage);
if (sectionA !== sectionB) return sectionA - sectionB;
return a.overallNumber - b.overallNumber;
})
.map(({ card }) => card);
}

return cards;
};

export const emptyScorecardsForPersons = (
wcif,
personsWithoutGroups,
language = 'en'
) => {
const {
localNamesFirst,
printOneName,
printStations,
scorecardPaperSize,
scorecardOrder,
} = getExtensionData('CompetitionConfig', wcif);

const { scorecardsPerPage } = scorecardPaperSizeInfos[scorecardPaperSize];

let cards = flatMap(personsWithoutGroups, ({ person, eventIds }) =>
flatMap(eventIds, eventId => {
const round = wcif.events
.find(e => e.id === eventId)
.rounds.find(r => r.id === `${eventId}-r1`);
const roundFormat = round.format;
const attemptCount = maxAttemptCountByFormat[roundFormat];

const card = scorecard({
competitionName: wcif.shortName,
activityCode: `${eventId}-r1`,
attemptCount,
round,
competitor: person,
localNamesFirst,
printOneName,
printStations,
scorecardPaperSize,
language,
printScrambleCheckerBox: shouldPrintScrambleChecker(
person,
round,
wcif
),
});

return card ? [card] : [];
})
);

return prepareScorecardsPages(cards, scorecardsPerPage, scorecardOrder);
};

export const downloadBlankScorecards = (wcif, language) => {
const { scorecardsBackgroundUrl, scorecardPaperSize } = getExtensionData(
'CompetitionConfig',
Expand Down Expand Up @@ -174,25 +280,31 @@ const scorecards = (wcif, rounds, rooms, language) => {
scorecardOrder,
printScorecardsCoverSheets,
} = getExtensionData('CompetitionConfig', wcif);

const { scorecardsPerPage } = scorecardPaperSizeInfos[scorecardPaperSize];

let cards = flatMap(rounds, round => {
const groupsWithCompetitors = groupActivitiesWithCompetitors(
wcif,
round.id
).filter(([groupActivity, _]) =>
rooms.includes(roomByActivity(wcif, groupActivity.id))
);

let scorecardNumber = sum(
groupsWithCompetitors.map(
([_, competitorsWithStation]) => competitorsWithStation.length
)
);

return flatMap(
groupsWithCompetitors,
([groupActivity, competitorsWithStation]) => {
const { featuredCompetitorWcaUserIds = [] } =
getExtensionData('ActivityConfig', groupActivity) || {};

let scorecardInGroupNumber = competitorsWithStation.length;

const groupCoverSheet = printScorecardsCoverSheets
? coverSheet({
competitionName: wcif.shortName,
Expand All @@ -201,11 +313,11 @@ const scorecards = (wcif, rounds, rooms, language) => {
room: roomByActivity(wcif, groupActivity.id),
})
: null;

const groupScorecards = competitorsWithStation.map(
([competitor, stationNumber]) =>
scorecard({
scorecardNumber: scorecardNumber--,
// If station numbers are assigned use those, otherwise generate on the fly
stationNumber: stationNumber || scorecardInGroupNumber--,
competitionName: wcif.shortName,
activityCode: groupActivity.activityCode,
Expand All @@ -227,9 +339,11 @@ const scorecards = (wcif, rounds, rooms, language) => {
),
})
);

if (groupCoverSheet) {
groupScorecards.unshift(groupCoverSheet);
}

const scorecardsOnLastPage = groupScorecards.length % scorecardsPerPage;
return scorecardsOnLastPage === 0 || scorecardOrder === 'stacked'
? groupScorecards
Expand All @@ -239,30 +353,8 @@ const scorecards = (wcif, rounds, rooms, language) => {
}
);
});
if (scorecardOrder === 'stacked') {
const scorecardsOnLastPage = cards.length % scorecardsPerPage;
if (scorecardsOnLastPage !== 0) {
cards = cards.concat(
times(scorecardsPerPage - scorecardsOnLastPage, () => ({}))
);
}
cards = cards
.map((card, idx) => {
return { overallNumber: idx, card };
})
.sort((cardA, cardB) => {
const sectionA =
cardA.overallNumber % (cards.length / scorecardsPerPage);
const sectionB =
cardB.overallNumber % (cards.length / scorecardsPerPage);
if (sectionA !== sectionB) {
return sectionA - sectionB;
}
return cardA.overallNumber - cardB.overallNumber;
})
.map(card => card.card);
}
return cards;

return prepareScorecardsPages(cards, scorecardsPerPage, scorecardOrder);
};

const shouldPrintScrambleChecker = (competitor, round, wcif) => {
Expand Down