Skip to content

Commit c0dd937

Browse files
New UI For scheduler (#1089)
Co-authored-by: shachar742 <shachar742@users.noreply.github.com>
1 parent b1ae898 commit c0dd937

File tree

15 files changed

+1269
-460
lines changed

15 files changed

+1269
-460
lines changed

apps/backend/src/routers/api/admin/divisions/schedule.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ router.post('/parse', fileUpload(), async (req: Request, res: Response) => {
7272
}
7373
});
7474

75-
router.post('/generate', async (req: Request, res: Response) => {
75+
const withScheduleRequest = async (req: Request, res: Response, next: () => void) => {
7676
const settings: ScheduleGenerationSettings = req.body;
7777

7878
const division = await db.getDivision({ _id: new ObjectId(req.params.divisionId) });
@@ -84,9 +84,6 @@ router.post('/generate', async (req: Request, res: Response) => {
8484
}
8585

8686
try {
87-
const domain = process.env.SCHEDULER_URL;
88-
if (!domain) throw new Error('SCHEDULER_URL is not configured');
89-
9087
const matchesStart = dayjs(settings.matchesStart);
9188
settings.matchesStart = dayjs(event.startDate)
9289
.set('minutes', matchesStart.get('minutes'))
@@ -121,10 +118,52 @@ router.post('/generate', async (req: Request, res: Response) => {
121118
}))
122119
};
123120

121+
req.body.schedulerRequest = schedulerRequest;
122+
return next();
123+
} catch {
124+
console.log('❌ Error parsing schedule request');
125+
res.status(400).json({ error: 'BAD_REQUEST' });
126+
}
127+
};
128+
129+
router.post('/validate', withScheduleRequest, async (req: Request, res: Response) => {
130+
try {
131+
const domain = process.env.SCHEDULER_URL;
132+
if (!domain) throw new Error('SCHEDULER_URL is not configured');
133+
134+
const response = await fetch(`${domain}/scheduler/validate`, {
135+
method: 'POST',
136+
headers: { 'Content-Type': 'application/json' },
137+
body: JSON.stringify(req.body.schedulerRequest)
138+
});
139+
140+
const data = await response.json();
141+
if (!response.ok && !data.error) throw new Error('Scheduler failed to run');
142+
if (data.is_valid) {
143+
res.status(200).json({ ok: true });
144+
return;
145+
}
146+
147+
res.status(400).json({ error: data.error });
148+
} catch (error) {
149+
console.log('❌ Error validating schedule');
150+
console.debug(error);
151+
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
152+
}
153+
});
154+
155+
router.post('/generate', withScheduleRequest, async (req: Request, res: Response) => {
156+
try {
157+
const domain = process.env.SCHEDULER_URL;
158+
if (!domain) throw new Error('SCHEDULER_URL is not configured');
159+
160+
const division = await db.getDivision({ _id: new ObjectId(req.params.divisionId) });
161+
const event = await db.getFllEvent({ _id: division.eventId });
162+
124163
await fetch(`${domain}/scheduler`, {
125164
method: 'POST',
126165
headers: { 'Content-Type': 'application/json' },
127-
body: JSON.stringify(schedulerRequest)
166+
body: JSON.stringify(req.body.schedulerRequest)
128167
}).then(res => {
129168
if (!res.ok) throw new Error('Scheduler failed to run');
130169
});
@@ -134,6 +173,7 @@ router.post('/generate', async (req: Request, res: Response) => {
134173
} catch (error) {
135174
console.log('❌ Error generating schedule');
136175
console.debug(error);
176+
const division = await db.getDivision({ _id: new ObjectId(req.params.divisionId) });
137177
await cleanDivisionData(division, true);
138178
console.log('✅ Deleted division data!');
139179
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });

apps/frontend/components/admin/division-schedule-editor.tsx

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
import { useState } from 'react';
22
import { WithId } from 'mongodb';
3-
import { useRouter } from 'next/router';
43
import { Box, Stack, Step, StepLabel, Stepper, Paper } from '@mui/material';
54
import { Division, FllEvent, ScheduleGenerationSettings } from '@lems/types';
65
import UploadTeamsStep from './schedule-generator/upload-teams-step';
76
import VenueSetupStep from './schedule-generator/venue-setup-step';
87
import TimingStep from './schedule-generator/timing-step';
98
import DeleteDivisionData from './delete-division-data';
10-
import ReviewStep from './schedule-generator/review-step';
11-
import { apiFetch } from 'apps/frontend/lib/utils/fetch';
9+
import { apiFetch } from '../../lib/utils/fetch';
1210
import { enqueueSnackbar } from 'notistack';
11+
import { useRouter } from 'next/router';
1312

14-
const SchedulerStages = ['upload-teams', 'venue-setup', 'timing', 'review'];
13+
const SchedulerStages = ['upload-teams', 'venue-setup', 'timing'];
1514
export type SchedulerStage = (typeof SchedulerStages)[number];
1615

1716
const localizedStages: Record<SchedulerStage, string> = {
1817
'upload-teams': 'העלאת קבוצות',
1918
'venue-setup': 'פרטי תחרות',
20-
timing: 'הגדרת זמנים',
21-
review: 'סיכום'
19+
timing: 'הגדרת זמנים'
2220
};
2321

2422
interface DivisionScheduleEditorProps {
@@ -78,32 +76,28 @@ const DivisionScheduleEditor: React.FC<DivisionScheduleEditorProps> = ({ divisio
7876
)}
7977
{activeStep === 2 && (
8078
<TimingStep
79+
event={event}
8180
division={division}
8281
settings={settings}
8382
updateSettings={setSettings}
84-
advanceStep={() => setActiveStep(3)}
85-
goBack={() => setActiveStep(1)}
86-
/>
87-
)}
88-
{activeStep === 3 && (
89-
<ReviewStep
90-
division={division}
91-
settings={settings}
92-
advanceStep={() => {
93-
apiFetch(`/api/admin/divisions/${division._id}/schedule/generate`, {
94-
method: 'POST',
95-
body: JSON.stringify(settings),
96-
headers: { 'Content-Type': 'application/json' }
97-
}).then(res => {
98-
if (res.ok) {
99-
enqueueSnackbar('לו"ז נוצר בהצלחה', { variant: 'success' });
100-
router.reload();
101-
} else {
102-
enqueueSnackbar('שגיאה ביצירת הלו"ז', { variant: 'error' });
83+
advanceStep={async () => {
84+
const response = await apiFetch(
85+
`/api/admin/divisions/${division._id}/schedule/generate`,
86+
{
87+
method: 'POST',
88+
body: JSON.stringify(settings),
89+
headers: { 'Content-Type': 'application/json' }
10390
}
104-
});
91+
);
92+
93+
if (response.ok) {
94+
enqueueSnackbar('לו"ז נוצר בהצלחה', { variant: 'success' });
95+
router.reload();
96+
} else {
97+
enqueueSnackbar('שגיאה ביצירת הלו"ז', { variant: 'error' });
98+
}
10599
}}
106-
goBack={() => setActiveStep(2)}
100+
goBack={() => setActiveStep(1)}
107101
/>
108102
)}
109103
</Box>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import dayjs, { Dayjs } from 'dayjs';
2+
import { useState } from 'react';
3+
import { Box } from '@mui/material';
4+
import { CalendarEvent, MINUTES_PER_SLOT, HOVER_AREA_HEIGHT, TIME_SLOT_HEIGHT } from './common';
5+
6+
export const BreakIndicator: React.FC<{
7+
topEvent: CalendarEvent;
8+
bottomEvent: CalendarEvent;
9+
startTime: Dayjs;
10+
onClick: () => void;
11+
}> = ({ topEvent, bottomEvent, startTime, onClick }) => {
12+
const [isHovered, setIsHovered] = useState(false);
13+
14+
const topEventEnd = dayjs(topEvent.endTime);
15+
const bottomEventStart = dayjs(bottomEvent.startTime);
16+
const gap = bottomEventStart.diff(topEventEnd, 'minute');
17+
18+
const topOffset =
19+
(dayjs(topEvent.endTime).diff(startTime, 'minute') / MINUTES_PER_SLOT) * TIME_SLOT_HEIGHT;
20+
21+
return (
22+
<Box
23+
sx={breakIndicatorStyle(topOffset, isHovered)}
24+
onMouseEnter={() => setIsHovered(true)}
25+
onMouseLeave={() => setIsHovered(false)}
26+
onClick={onClick}
27+
data-gap={`${gap}min`}
28+
/>
29+
);
30+
};
31+
32+
const breakIndicatorStyle = (topOffset: number, isHovered: boolean) => ({
33+
position: 'absolute',
34+
width: '95%',
35+
height: `${HOVER_AREA_HEIGHT}px`,
36+
top: topOffset - HOVER_AREA_HEIGHT / 2,
37+
backgroundColor: isHovered ? 'rgba(25, 118, 210, 0.2)' : 'transparent',
38+
cursor: 'pointer',
39+
transition:
40+
'background-color 0.2s ease-in-out, transform 0.2s ease-in-out, opacity 0.2s ease-in-out',
41+
display: 'flex',
42+
alignItems: 'center',
43+
justifyContent: 'center',
44+
'&:hover': {
45+
backgroundColor: 'rgba(25, 118, 210, 0.2)',
46+
'&::after': {
47+
content: '"הוסף הפסקה"',
48+
position: 'absolute',
49+
left: '105%',
50+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
51+
color: 'white',
52+
padding: '4px 8px',
53+
borderRadius: '4px',
54+
fontSize: '12px',
55+
whiteSpace: 'nowrap',
56+
zIndex: 1000
57+
}
58+
},
59+
'&::before': {
60+
content: '""',
61+
position: 'absolute',
62+
width: '100%',
63+
height: '2px',
64+
backgroundColor: isHovered ? 'rgba(25, 118, 210, 0.4)' : 'rgba(25, 118, 210, 0.1)'
65+
},
66+
zIndex: 3
67+
});

0 commit comments

Comments
 (0)