Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/admin/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@
"title": "Publish Event",
"message": "Are you sure you want to publish this event? This will make the results public and visible to everyone.",
"event-name": "Event: {eventName}",
"info": "If SendGrid integration is enabled, emails will be sent to team contacts with their results.",
"publishing-progress": "Publishing event...",
"confirm": "Publish Event",
"cancel": "Cancel",
"publishing": "Publishing..."
Expand All @@ -666,6 +668,7 @@
"message": "Download results as a ZIP file? This will include rubrics and scoresheets for all teams registered in this event.",
"event-name": "Event: {eventName}",
"info": "The ZIP file will be organized by team slug (e.g.,9999-IL) with PDFs inside each folder.",
"generating": "Generating results...",
"confirm": "Download",
"cancel": "Cancel",
"downloading": "Downloading..."
Expand All @@ -677,6 +680,7 @@
"complete-success": "Event completed successfully",
"complete-error": "Failed to complete event",
"publish-success": "Event published successfully",
"publish-success-with-emails": "Event published successfully! Sent {emailsSent} emails ({emailsFailed} failed).",
"publish-error": "Failed to publish event",
"download-success": "Event results downloaded successfully",
"download-error": "Failed to download event results",
Expand Down
4 changes: 4 additions & 0 deletions apps/admin/locale/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@
"title": "פרסום אירוע",
"message": "האם אתם בטוחים שברצונכם לפרסם את האירוע? פעולה זו תהפוך את התוצאות לציבוריות ונראות לכולם.",
"event-name": "אירוע: {eventName}",
"info": "אם קיימת אינטגרציה עם SendGrid, יישלחו הודעות דוא\"ל ליצירי קשר של הקבוצה עם התוצאות שלהם.",
"publishing-progress": "מפרסם תוצאות...",
"confirm": "פרסום אירוע",
"cancel": "ביטול",
"publishing": "מפרסם..."
Expand All @@ -666,6 +668,7 @@
"message": "להוריד תוצאות כקובץ ZIP? זה יכלול מחווני שיפוט ודפי ניקוד לכל הקבוצות הרשומות באירוע זה.",
"event-name": "אירוע: {eventName}",
"info": "קובץ ה-ZIP יהיה מאורגן לפי סימון הקבוצה (לדוגמה, 9999-IL) עם קבצי PDF בתוך כל תיקייה.",
"generating": "מייצר תוצאות...",
"confirm": "הורדה",
"cancel": "ביטול",
"downloading": "מוריד..."
Expand All @@ -677,6 +680,7 @@
"complete-success": "האירוע הושלם בהצלחה",
"complete-error": "סיום האירוע נכשל",
"publish-success": "האירוע פורסם בהצלחה",
"publish-success-with-emails": "האירוע פורסם בהצלחה! נשלחו {emailsSent} הודעות דוא\"ל ({emailsFailed} נכשלו).",
"publish-error": "פרסום האירוע נכשל",
"download-success": "תוצאות האירוע הורדו בהצלחה",
"download-error": "הורדת תוצאות האירוע נכשלה",
Expand Down
4 changes: 4 additions & 0 deletions apps/admin/locale/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@
"title": "Opublikuj wydarzenie",
"message": "Czy na pewno chcesz opublikować to wydarzenie? Wyniki będą widoczne dla wszystkich.",
"event-name": "Wydarzenie: {eventName}",
"info": "Jeśli integracja SendGrid jest włączona, wiadomości e-mail z wynikami będą wysłane do kontaktów drużyn.",
"publishing-progress": "Publikowanie wydarzenia...",
"confirm": "Opublikuj wydarzenie",
"cancel": "Anuluj",
"publishing": "Publikowanie..."
Expand All @@ -666,6 +668,7 @@
"message": "Pobrać wyniki jako plik ZIP? Będzie zawierać rubryki i karty oceny dla wszystkich drużyn zarejestrowanych w tym wydarzeniu.",
"event-name": "Wydarzenie: {eventName}",
"info": "Plik ZIP będzie zorganizowany według identyfikatora drużyny (np. 9999-IL) z plikami PDF w każdym folderze.",
"generating": "Generowanie wyników...",
"confirm": "Pobierz",
"cancel": "Anuluj",
"downloading": "Pobieranie..."
Expand All @@ -677,6 +680,7 @@
"complete-success": "Wydarzenie zakończono pomyślnie",
"complete-error": "Nie udało się zakończyć wydarzenia",
"publish-success": "Wydarzenie opublikowano pomyślnie",
"publish-success-with-emails": "Wydarzenie opublikowano pomyślnie! Wysłano {emailsSent} wiadomości e-mail ({emailsFailed} nie powiodło się).",
"publish-error": "Nie udało się opublikować wydarzenia",
"download-success": "Wyniki wydarzenia pobrano pomyślnie",
"download-error": "Nie udało się pobrać wyników wydarzenia",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
DialogActions,
Typography,
CircularProgress,
Stack
LinearProgress,
Stack,
Box
} from '@mui/material';

interface DownloadResultsDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onConfirm: (onProgress: (percent: number) => void) => Promise<void>;
eventName: string;
}

Expand All @@ -27,14 +29,17 @@ export const DownloadResultsDialog: React.FC<DownloadResultsDialogProps> = ({
eventName
}) => {
const [isDownloading, setIsDownloading] = useState(false);
const [progress, setProgress] = useState<number | null>(null);
const t = useTranslations('pages.events.settings.dialogs.download-results');

const handleConfirm = async () => {
setIsDownloading(true);
setProgress(0);
try {
await onConfirm();
await onConfirm(setProgress);
} finally {
setIsDownloading(false);
setProgress(null);
}
};

Expand All @@ -50,6 +55,14 @@ export const DownloadResultsDialog: React.FC<DownloadResultsDialogProps> = ({
<Typography variant="caption" color="text.secondary">
{t('info')}
</Typography>
{progress !== null && (
<Box>
<Typography variant="caption" color="text.secondary">
{t('generating')}
</Typography>
<LinearProgress variant="determinate" value={progress} sx={{ mt: 0.5 }} />
</Box>
)}
</Stack>
</DialogContent>
<DialogActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { KeyedMutator } from 'swr';
import { useLocale, useTranslations } from 'next-intl';
import { Box, Card, CardContent, Typography, Button, Divider, Stack } from '@mui/material';
import { EventSettings } from '@lems/types/api/admin';
import { apiFetch } from '@lems/shared';
import { apiFetch, connectSseStream, getApiBase } from '@lems/shared';
import { useEvent } from '../../components/event-context';
import { CompleteEventDialog } from './complete-event-dialog';
import { PublishEventDialog } from './publish-event-dialog';
Expand All @@ -29,7 +29,6 @@ export const EventActionsSection: React.FC<EventActionsSectionProps> = ({
const [completeDialogOpen, setCompleteDialogOpen] = useState(false);
const [publishDialogOpen, setPublishDialogOpen] = useState(false);
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
const [isDownloadLoading, setIsDownloadLoading] = useState(false);

const handleCompleteEvent = async () => {
setAlert(null);
Expand All @@ -50,57 +49,71 @@ export const EventActionsSection: React.FC<EventActionsSectionProps> = ({
}
};

const handlePublishEvent = async () => {
const handlePublishEvent = async (onProgress: (percent: number, message?: string) => void) => {
setAlert(null);
try {
const response = await apiFetch(`/admin/events/${event.id}/settings/publish`, {
method: 'POST'
});
const result = await connectSseStream<{
published: boolean;
emailsSent: number;
emailsFailed: number;
failedEmails?: string[];
}>(
`/admin/events/${event.id}/settings/publish`,
{ method: 'POST' },
{
onStart: () => onProgress(0),
onProgress: (percent, message) => onProgress(percent, message)
}
);

if (response.ok) {
if (result?.published) {
await mutateSettings();
setAlert({ type: 'success', message: t('messages.publish-success') });
const successMsg =
result.emailsSent > 0
? t('messages.publish-success-with-emails', {
emailsSent: result.emailsSent,
emailsFailed: result.emailsFailed
})
: t('messages.publish-success');
setAlert({ type: 'success', message: successMsg });
setPublishDialogOpen(false);
} else {
setAlert({ type: 'error', message: t('messages.publish-error') });
}
} catch {
setAlert({ type: 'error', message: t('messages.publish-error') });
} catch (error) {
const errorMsg =
error instanceof Error ? error.message : t('messages.publish-error');
setAlert({ type: 'error', message: errorMsg });
}
};

const handleDownloadResults = async () => {
const handleDownloadResults = async (onProgress: (percent: number) => void) => {
setAlert(null);
setIsDownloadLoading(true);
try {
const response = await apiFetch(
const result = await connectSseStream<{ token: string }>(
`/admin/events/${event.id}/settings/download?language=${locale}`,
{ method: 'POST' },
{
method: 'POST',
responseType: 'binary'
onStart: () => onProgress(0),
onProgress
}
);

if (response.ok) {
const blob = response.data as Blob;
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${event.name.replace(/\s+/g, '_')}-results.zip`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);

setAlert({ type: 'success', message: t('messages.download-success') });
setDownloadDialogOpen(false);
} else {
if (!result?.token) {
setAlert({ type: 'error', message: t('messages.download-error') });
return;
}

const a = document.createElement('a');
a.href = `${getApiBase(true)}/admin/events/${event.id}/settings/download/file?token=${result.token}`;
document.body.appendChild(a);
a.click();
a.remove();

setAlert({ type: 'success', message: t('messages.download-success') });
setDownloadDialogOpen(false);
} catch {
Comment thread
johnmeshulam marked this conversation as resolved.
setAlert({ type: 'error', message: t('messages.download-error') });
} finally {
setIsDownloadLoading(false);
}
};

Expand Down Expand Up @@ -173,7 +186,6 @@ export const EventActionsSection: React.FC<EventActionsSectionProps> = ({
disabled={!settings.published}
onClick={() => setDownloadDialogOpen(true)}
sx={{ minWidth: 160 }}
loading={isDownloadLoading}
>
{t('event-actions.download-results')}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import {
DialogContent,
DialogActions,
Typography,
CircularProgress
CircularProgress,
LinearProgress,
Stack,
Box,
Alert
} from '@mui/material';

interface PublishEventDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onConfirm: (onProgress: (percent: number, message?: string) => void) => Promise<void>;
eventName: string;
}

Expand All @@ -26,27 +30,58 @@ export const PublishEventDialog: React.FC<PublishEventDialogProps> = ({
eventName
}) => {
const [isPublishing, setIsPublishing] = useState(false);
const [progress, setProgress] = useState<number | null>(null);
const [progressMessage, setProgressMessage] = useState<string | null>(null);
const t = useTranslations('pages.events.settings.dialogs.publish-event');

const handleConfirm = async () => {
setIsPublishing(true);
setProgress(0);
setProgressMessage(null);
try {
await onConfirm();
await onConfirm((percent, message) => {
setProgress(percent);
if (message) setProgressMessage(message);
});
} finally {
setIsPublishing(false);
setProgress(null);
setProgressMessage(null);
}
};

return (
<Dialog open={open} onClose={isPublishing ? undefined : onClose} maxWidth="sm" fullWidth>
<DialogTitle>{t('title')}</DialogTitle>
<DialogContent>
<Typography>
{t('message')}
</Typography>
<Typography variant="body2" sx={{ mt: 2, fontWeight: 'bold' }}>
{t('event-name', { eventName })}
</Typography>
<Stack spacing={2}>
<Typography>{t('message')}</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{t('event-name', { eventName })}
</Typography>
<Typography variant="caption" color="text.secondary">
{t('info')}
</Typography>
{progress !== null && (
<Box>
{progressMessage ? (
<Alert severity="info" sx={{ mb: 1 }}>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
{progressMessage}
</Typography>
<LinearProgress variant="determinate" value={progress} />
</Alert>
) : (
<Box>
<Typography variant="caption" color="text.secondary">
{t('publishing-progress')}
</Typography>
<LinearProgress variant="determinate" value={progress} sx={{ mt: 0.5 }} />
</Box>
)}
</Box>
)}
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={isPublishing}>
Expand Down
14 changes: 13 additions & 1 deletion apps/backend/src/lib/results-download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,19 @@ async function getZippedResults(
eventSlug: string,
archive: archiver.Archiver,
language: string,
onProgress?: (percent: number) => void,
batchSize: number = 10
): Promise<{
totalTeams: number;
teamsWithPdfs: number;
failedPdfs: number;
}> {
const registeredTeams = await db.events.byId(eventId).getRegisteredTeams();
const totalTeamCount = registeredTeams.length;

const teamsWithResults = new Set<string>();
let totalTeams = 0;
let teamsProcessed = 0;
let failedPdfs = 0;
let batchNumber = 0;

Expand Down Expand Up @@ -153,6 +158,11 @@ async function getZippedResults(
} else {
failedPdfs++;
}

teamsProcessed++;
if (totalTeamCount > 0) {
onProgress?.(Math.round((teamsProcessed / totalTeamCount) * 100));
}
}

console.info(
Expand Down Expand Up @@ -189,7 +199,8 @@ async function getZippedResults(
*/
export async function generateEventResultsZip(
eventId: string,
language: string = 'en'
language: string = 'en',
onProgress?: (percent: number) => void
): Promise<{
archive: archiver.Archiver;
fileName: string;
Expand All @@ -210,6 +221,7 @@ export async function generateEventResultsZip(
event.slug,
archive,
language,
onProgress,
5 // Process 5 teams at a time
);

Expand Down
Loading
Loading