Compare commits

...

5 Commits

7 changed files with 126 additions and 38 deletions

View File

@ -224,25 +224,25 @@ async function updateScheduleSettings() {
const staffList = document.getElementById('removeStaff'); const staffList = document.getElementById('removeStaff');
utils.clearChildren(staffList); utils.clearChildren(staffList);
schedule.notAssignedStaff.forEach(name => { schedule.all_staff_teachers_first.forEach(name => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = name; option.value = name;
option.innerText = name; option.innerText = (schedule.assignedStaff.has(name) ? '(!) ' : '') + name;
staffList.appendChild(option); staffList.appendChild(option);
}); });
const canDelStaff = allowEdits && schedule.notAssignedStaff.size > 0; const canDelStaff = allowEdits;
staffList.disabled = !canDelStaff; staffList.disabled = !canDelStaff;
document.getElementById('delStaff').disabled = !canDelStaff; document.getElementById('delStaff').disabled = !canDelStaff;
const studentList = document.getElementById('removeStudent'); const studentList = document.getElementById('removeStudent');
utils.clearChildren(studentList); utils.clearChildren(studentList);
schedule.notAssignedStudents.forEach(name => { schedule.all_students.forEach(name => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = name; option.value = name;
option.innerText = name; option.innerText = (schedule.assignedStudents.has(name) ? '(!) ' : '') + name;
studentList.appendChild(option); studentList.appendChild(option);
}); });
const canDelStudent = allowEdits && schedule.notAssignedStudents.size > 0; const canDelStudent = allowEdits;
studentList.disabled = !canDelStudent; studentList.disabled = !canDelStudent;
document.getElementById('delStudent').disabled = !canDelStudent; document.getElementById('delStudent').disabled = !canDelStudent;
@ -731,12 +731,36 @@ document.getElementById('addTeacher').addEventListener('click', async e => {
}); });
document.getElementById('delStudent').addEventListener('click', async e => { document.getElementById('delStudent').addEventListener('click', async e => {
schedule.delStudent(document.getElementById('removeStudent').value); const name = document.getElementById('removeStudent').value;
const person = {name, kind: 'student'};
if (schedule.assignedStudents.has(name)) {
if (!window.confirm("Are you sure you want to delete the student " + name
+ " who is still assigned to events?")) {
return;
}
schedule.assignments.filter(a => a.hasPersonExplicitlyOnAnyDay(person))
.forEach(a => a.removePerson(person));
schedule.recomputeNotAssigned();
}
schedule.delStudent(name);
await saveSchedule(); await saveSchedule();
}); });
document.getElementById('delStaff').addEventListener('click', async e => { document.getElementById('delStaff').addEventListener('click', async e => {
schedule.delStaff(document.getElementById('removeStaff').value); const name = document.getElementById('removeStaff').value;
const person = {name, kind: 'staff'};
if (schedule.assignedStudents.has(name)) {
if (!window.confirm("Are you sure you want to delete the staff person "
+ name + " who is still assigned to events?")) {
return;
}
schedule.assignments.filter(a => a.hasPersonExplicitlyOnAnyDay(person))
.forEach(a => a.removePerson(person));
schedule.recomputeNotAssigned();
}
schedule.delStaff(name);
await saveSchedule(); await saveSchedule();
}); });
@ -981,7 +1005,7 @@ function initializeAssignmentForm(assignment) {
assignmentForm.end_time.value = assignment.end_time.inputValue; assignmentForm.end_time.value = assignment.end_time.inputValue;
assignmentForm.squishable.value = assignment.squishable; assignmentForm.squishable.value = assignment.squishable;
assignmentForm.track.value = Array.isArray(assignment.track) assignmentForm.track.value = Array.isArray(assignment.track)
? assignment.track.join(',') ? assignment.track[0]
: (assignment.track ?? "auto"); : (assignment.track ?? "auto");
assignmentForm.notes.value = assignment.notes ?? ""; assignmentForm.notes.value = assignment.notes ?? "";
@ -1026,16 +1050,28 @@ assignmentForm.squishable.addEventListener('change', async e => {
await saveSchedule(); await saveSchedule();
}); });
assignmentForm.track.addEventListener('change', async e => { async function setAssignmentTrackAndWidth() {
const str = e.target.selectedOptions[0].value; const width = parseInt(assignmentForm.width.value);
selectedAssignment.width = width;
const str = assignmentForm.track.selectedOptions[0].value;
if (str === 'auto') selectedAssignment.track = undefined; if (str === 'auto') selectedAssignment.track = undefined;
else if (str === 'all') selectedAssignment.track = 'all'; else if (str === 'all') selectedAssignment.track = 'all';
else if (str.includes(',')) { else {
selectedAssignment.track = str.split(',').map(s => parseInt(s)); const track = parseInt(str);
} else selectedAssignment.track = parseInt(str); if (width === 1) selectedAssignment.track = track;
else {
let tracks = [];
for (let i = track; i < track + width; i++) tracks.push(i);
selectedAssignment.track = tracks;
}
}
await saveSchedule(); await saveSchedule();
}); }
assignmentForm.width.addEventListener('change', setAssignmentTrackAndWidth);
assignmentForm.track.addEventListener('change', setAssignmentTrackAndWidth);
assignmentForm.notes.addEventListener('change', async e => { assignmentForm.notes.addEventListener('change', async e => {
selectedAssignment.notes = e.target.value; selectedAssignment.notes = e.target.value;

View File

@ -8,6 +8,7 @@ export default class Assignment {
this.start_time = new utils.Time(obj.start_time); this.start_time = new utils.Time(obj.start_time);
this.end_time = new utils.Time(obj.end_time); this.end_time = new utils.Time(obj.end_time);
this.notes = obj.notes; this.notes = obj.notes;
this.width = obj.width ?? (Array.isArray(obj.track) ? obj.track.length : 1);
this.track = obj.track; this.track = obj.track;
this.days = obj.days; this.days = obj.days;
this.people_by_day = obj.people_by_day; this.people_by_day = obj.people_by_day;
@ -32,6 +33,10 @@ export default class Assignment {
this.days = expectedDays; this.days = expectedDays;
} }
this.recomputeAllPeople();
}
recomputeAllPeople() {
this.all_staff = new Set(Object.values(this.people_by_day) this.all_staff = new Set(Object.values(this.people_by_day)
.flatMap(day => day['staff'])); .flatMap(day => day['staff']));
this.all_students = new Set(Object.values(this.people_by_day) this.all_students = new Set(Object.values(this.people_by_day)
@ -44,6 +49,7 @@ export default class Assignment {
'start_time': this.start_time.asJsonObject(), 'start_time': this.start_time.asJsonObject(),
'end_time': this.end_time.asJsonObject(), 'end_time': this.end_time.asJsonObject(),
'notes': this.notes, 'notes': this.notes,
'width': this.width,
'track': this.track, 'track': this.track,
'days': this.days, 'days': this.days,
'people_by_day': this.people_by_day, 'people_by_day': this.people_by_day,
@ -116,6 +122,17 @@ export default class Assignment {
.some(byDay => byDay[kind].includes(person.name)); .some(byDay => byDay[kind].includes(person.name));
} }
removePerson(person) {
const kind = person.kind === 'student' ? 'students' : person.kind;
Object.values(this.people_by_day)
.forEach(byDay => {
const kindList = byDay[kind];
const idx = kindList.indexOf(person.name);
if (idx !== -1) kindList.splice(idx, 1);
});
this.recomputeAllPeople();
}
asEvent(day, granularity, location_only) { asEvent(day, granularity, location_only) {
const people = this.people_by_day[day]; const people = this.people_by_day[day];
const staff = people['staff'] ?? []; const staff = people['staff'] ?? [];
@ -203,6 +220,8 @@ export default class Assignment {
'assignment': originalAssignment, 'assignment': originalAssignment,
'start_time': this.start_time, 'start_time': this.start_time,
'end_time': this.end_time, 'end_time': this.end_time,
'day': day,
'width': this.width,
'track': this.track, 'track': this.track,
'description': description, 'description': description,
'classes': classes, 'classes': classes,

View File

@ -5,14 +5,16 @@ export default class Event {
this.assignment = args.assignment; this.assignment = args.assignment;
this.start_time = args.start_time; this.start_time = args.start_time;
this.end_time = args.end_time; this.end_time = args.end_time;
this.width = args.width;
this.track = args.track; this.track = args.track;
this.description = args.description ?? "ERROR: MISSING DESCRIPTION"; this.description = args.description ?? "ERROR: MISSING DESCRIPTION";
this.classes = args.classes ?? []; this.classes = args.classes ?? [];
this.title = args.title; this.title = args.title;
this.day = args.day;
} }
clone() { clone() {
return new Event(this); return new Event({...this, description: this.description.cloneNode(true)});
} }
get tracks() { get tracks() {

View File

@ -84,16 +84,19 @@
<label>Location: <input name="location" value=""></label><br> <label>Location: <input name="location" value=""></label><br>
<label>Start time: <input type="time" required="" name="start_time"></label><br> <label>Start time: <input type="time" required="" name="start_time"></label><br>
<label>End time: <input type="time" required="" name="end_time"></label><br> <label>End time: <input type="time" required="" name="end_time"></label><br>
<label>Squishable: <input type="checkbox" name="squishable"></label><br> <label>
<acronym title="This event can be shortened (squished) or omitted in an indivudal schedule if overlapped by another event.">Squishable</acronym>:
<input type="checkbox" name="squishable">
</label><br>
<label>Width: <input name="width" type="number" min="1" max="9" value="1"></label><br>
<label>Track: <label>Track:
<select name="track"> <select name="track">
<option selected="" value="auto">auto</option> <option selected="" value="auto">auto</option>
<option value="all">all (full-width)</option> <option value="all">all (full-width)</option>
<option value="0,1">[0,1] left-most two tracks</option>
<option value="1,2">[1,2] second and third tracks</option>
<option value="0">[0] left-most</option> <option value="0">[0] left-most</option>
<option value="1,2,3">[1,2,3] second through fourth tracks</option> <option value="1">[1] second</option>
<option value="0,1,2">[0,1,2] left-most three tracks</option> <option value="2">[2] third</option>
<option value="3">[3] fourth</option>
</select> </select>
</label><br> </label><br>
<label>Notes: <textarea name="notes"></textarea></label><br> <label>Notes: <textarea name="notes"></textarea></label><br>

View File

@ -187,6 +187,10 @@ export default class Schedule {
return [...utils.setDifference(this.all_staff, this.all_teachers)].sort(); return [...utils.setDifference(this.all_staff, this.all_teachers)].sort();
} }
get all_staff_teachers_first() {
return [...this.all_teachers, ...this.all_non_teacher_staff];
}
get all_people() { get all_people() {
return [...this.all_teachers.concat(this.all_non_teacher_staff) return [...this.all_teachers.concat(this.all_non_teacher_staff)
.map(name => ({name, kind: 'staff'})), .map(name => ({name, kind: 'staff'})),
@ -198,6 +202,7 @@ export default class Schedule {
this.all_students.push(name); this.all_students.push(name);
this.all_students.sort(); this.all_students.sort();
this.notAssignedStudents.add(name); this.notAssignedStudents.add(name);
return true;
} }
addStaff(name) { addStaff(name) {
@ -205,6 +210,7 @@ export default class Schedule {
this.all_staff.push(name); this.all_staff.push(name);
this.all_staff.sort(); this.all_staff.sort();
this.notAssignedStaff.add(name); this.notAssignedStaff.add(name);
return true;
} }
addTeacher(name) { addTeacher(name) {
@ -212,6 +218,7 @@ export default class Schedule {
this.all_teachers.push(name); this.all_teachers.push(name);
this.all_teachers.sort(); this.all_teachers.sort();
this.notAssignedStaff.add(name); this.notAssignedStaff.add(name);
return true;
} }
delStaff(name) { delStaff(name) {

View File

@ -9,7 +9,8 @@ export default class ScheduleGrid {
this.events = ScheduleGrid.assignTracks(args.events); this.events = ScheduleGrid.assignTracks(args.events);
this.tracks = [...new Set(this.events.flatMap(e => e.tracks))].sort() this.tracks = [...new Set(this.events.flatMap(e => e.tracks))]
.sort((a, b) => a-b);
// If there's supposed to be only a single column but there's // If there's supposed to be only a single column but there's
// actually multiple tracks, make as many events as possible full width. // actually multiple tracks, make as many events as possible full width.
@ -50,21 +51,41 @@ export default class ScheduleGrid {
eventsWithoutTracks.sort((a, b) => a.start_time.cmp(b.start_time)); eventsWithoutTracks.sort((a, b) => a.start_time.cmp(b.start_time));
for (const event of eventsWithoutTracks) { for (const event of eventsWithoutTracks) {
// Put the event in the first track with space for it. // Put the event in the first track with space for it.
tracks.forEach((track, trackNum) => { const tracksWithSpace = Object.keys(tracks).map(t => parseInt(t)).filter(trackNum => {
if (event.track === undefined) { const end = Math.min(trackNum + event.width, tracks.length);
const hasOverlappingEvents = track.some(e => e.at_same_time_as(event)); for (let i = trackNum; i < end; i++) {
if (!hasOverlappingEvents) { if (tracks[i].some(e => e.at_same_time_as(event))) {
track.push(event); return false;
event.track = trackNum;
} }
} }
}); return true;
})
// No track had space for it, create a new track to put it in. .map(trackNum => {
if (event.track === undefined) { const staffLists = [...tracks[trackNum], event]
event.track = tracks.length; .map(e => e.assignment.people_by_day[e.day].staff)
tracks.push([event]); .filter(staff => staff.length > 0);
const matchingStaff = staffLists.length === 0
? []
: staffLists.reduce(utils.setIntersection);
return {
trackNum,
matchingStaff,
matchingStaffCount: matchingStaff.size,
};
})
.sort((a, b) => b.matchingStaffCount - a.matchingStaffCount)
.map(t => t.trackNum);
const trackNum = tracksWithSpace.length > 0 ? tracksWithSpace[0] : tracks.length;
const eventTracks = [];
for (let i = trackNum; i < trackNum + event.width; i++) {
eventTracks.push(i);
if (i < tracks.length) {
tracks[i].push(event);
} else {
tracks.push([event]);
}
} }
event.track = eventTracks.length === 1 ? eventTracks[0] : eventTracks;
} }
// The above code doesn't consider events with track "all". // The above code doesn't consider events with track "all".
@ -141,7 +162,7 @@ export default class ScheduleGrid {
// Select events of this track in this row. // Select events of this track in this row.
const events = this.events const events = this.events
.filter(e => (e.track === 'all' || e.tracks.includes(track)) .filter(e => (e.track === 'all' || e.tracks.includes(track))
&& e.start_time.cmp(nextRowTime) <0 && e.start_time.cmp(nextRowTime) < 0
&& rowTime.cmp(e.end_time) < 0); && rowTime.cmp(e.end_time) < 0);
if (events.length === 0) { if (events.length === 0) {
row.appendChild(document.createElement('td')); row.appendChild(document.createElement('td'));

View File

@ -1,8 +1,8 @@
export class Time { export class Time {
constructor(obj) { constructor(obj) {
this.hour = obj.hour ?? 0; this.hour = parseInt(obj.hour ?? 0);
this.minute = obj.minute ?? 0; this.minute = parseInt(obj.minute ?? 0);
this.second = obj.second ?? 0; this.second = parseInt(obj.second ?? 0);
} }
static fromInputValue(str) { static fromInputValue(str) {