Compare commits
5 Commits
36a0844951
...
0a26315e04
Author | SHA1 | Date | |
---|---|---|---|
0a26315e04 | |||
28500bdb10 | |||
34c2a4959e | |||
fe02b2a326 | |||
8882fefa9e |
66
www/app.js
66
www/app.js
|
@ -224,25 +224,25 @@ async function updateScheduleSettings() {
|
|||
|
||||
const staffList = document.getElementById('removeStaff');
|
||||
utils.clearChildren(staffList);
|
||||
schedule.notAssignedStaff.forEach(name => {
|
||||
schedule.all_staff_teachers_first.forEach(name => {
|
||||
const option = document.createElement('option');
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
option.innerText = (schedule.assignedStaff.has(name) ? '(!) ' : '') + name;
|
||||
staffList.appendChild(option);
|
||||
});
|
||||
const canDelStaff = allowEdits && schedule.notAssignedStaff.size > 0;
|
||||
const canDelStaff = allowEdits;
|
||||
staffList.disabled = !canDelStaff;
|
||||
document.getElementById('delStaff').disabled = !canDelStaff;
|
||||
|
||||
const studentList = document.getElementById('removeStudent');
|
||||
utils.clearChildren(studentList);
|
||||
schedule.notAssignedStudents.forEach(name => {
|
||||
schedule.all_students.forEach(name => {
|
||||
const option = document.createElement('option');
|
||||
option.value = name;
|
||||
option.innerText = name;
|
||||
option.innerText = (schedule.assignedStudents.has(name) ? '(!) ' : '') + name;
|
||||
studentList.appendChild(option);
|
||||
});
|
||||
const canDelStudent = allowEdits && schedule.notAssignedStudents.size > 0;
|
||||
const canDelStudent = allowEdits;
|
||||
studentList.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 => {
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -981,7 +1005,7 @@ function initializeAssignmentForm(assignment) {
|
|||
assignmentForm.end_time.value = assignment.end_time.inputValue;
|
||||
assignmentForm.squishable.value = assignment.squishable;
|
||||
assignmentForm.track.value = Array.isArray(assignment.track)
|
||||
? assignment.track.join(',')
|
||||
? assignment.track[0]
|
||||
: (assignment.track ?? "auto");
|
||||
assignmentForm.notes.value = assignment.notes ?? "";
|
||||
|
||||
|
@ -1026,16 +1050,28 @@ assignmentForm.squishable.addEventListener('change', async e => {
|
|||
await saveSchedule();
|
||||
});
|
||||
|
||||
assignmentForm.track.addEventListener('change', async e => {
|
||||
const str = e.target.selectedOptions[0].value;
|
||||
async function setAssignmentTrackAndWidth() {
|
||||
const width = parseInt(assignmentForm.width.value);
|
||||
selectedAssignment.width = width;
|
||||
|
||||
const str = assignmentForm.track.selectedOptions[0].value;
|
||||
if (str === 'auto') selectedAssignment.track = undefined;
|
||||
else if (str === 'all') selectedAssignment.track = 'all';
|
||||
else if (str.includes(',')) {
|
||||
selectedAssignment.track = str.split(',').map(s => parseInt(s));
|
||||
} else selectedAssignment.track = parseInt(str);
|
||||
else {
|
||||
const 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();
|
||||
});
|
||||
}
|
||||
|
||||
assignmentForm.width.addEventListener('change', setAssignmentTrackAndWidth);
|
||||
assignmentForm.track.addEventListener('change', setAssignmentTrackAndWidth);
|
||||
|
||||
assignmentForm.notes.addEventListener('change', async e => {
|
||||
selectedAssignment.notes = e.target.value;
|
||||
|
|
|
@ -8,6 +8,7 @@ export default class Assignment {
|
|||
this.start_time = new utils.Time(obj.start_time);
|
||||
this.end_time = new utils.Time(obj.end_time);
|
||||
this.notes = obj.notes;
|
||||
this.width = obj.width ?? (Array.isArray(obj.track) ? obj.track.length : 1);
|
||||
this.track = obj.track;
|
||||
this.days = obj.days;
|
||||
this.people_by_day = obj.people_by_day;
|
||||
|
@ -32,6 +33,10 @@ export default class Assignment {
|
|||
this.days = expectedDays;
|
||||
}
|
||||
|
||||
this.recomputeAllPeople();
|
||||
}
|
||||
|
||||
recomputeAllPeople() {
|
||||
this.all_staff = new Set(Object.values(this.people_by_day)
|
||||
.flatMap(day => day['staff']));
|
||||
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(),
|
||||
'end_time': this.end_time.asJsonObject(),
|
||||
'notes': this.notes,
|
||||
'width': this.width,
|
||||
'track': this.track,
|
||||
'days': this.days,
|
||||
'people_by_day': this.people_by_day,
|
||||
|
@ -116,6 +122,17 @@ export default class Assignment {
|
|||
.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) {
|
||||
const people = this.people_by_day[day];
|
||||
const staff = people['staff'] ?? [];
|
||||
|
@ -203,6 +220,8 @@ export default class Assignment {
|
|||
'assignment': originalAssignment,
|
||||
'start_time': this.start_time,
|
||||
'end_time': this.end_time,
|
||||
'day': day,
|
||||
'width': this.width,
|
||||
'track': this.track,
|
||||
'description': description,
|
||||
'classes': classes,
|
||||
|
|
|
@ -5,14 +5,16 @@ export default class Event {
|
|||
this.assignment = args.assignment;
|
||||
this.start_time = args.start_time;
|
||||
this.end_time = args.end_time;
|
||||
this.width = args.width;
|
||||
this.track = args.track;
|
||||
this.description = args.description ?? "ERROR: MISSING DESCRIPTION";
|
||||
this.classes = args.classes ?? [];
|
||||
this.title = args.title;
|
||||
this.day = args.day;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Event(this);
|
||||
return new Event({...this, description: this.description.cloneNode(true)});
|
||||
}
|
||||
|
||||
get tracks() {
|
||||
|
|
|
@ -84,16 +84,19 @@
|
|||
<label>Location: <input name="location" value=""></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>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:
|
||||
<select name="track">
|
||||
<option selected="" value="auto">auto</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="1,2,3">[1,2,3] second through fourth tracks</option>
|
||||
<option value="0,1,2">[0,1,2] left-most three tracks</option>
|
||||
<option value="1">[1] second</option>
|
||||
<option value="2">[2] third</option>
|
||||
<option value="3">[3] fourth</option>
|
||||
</select>
|
||||
</label><br>
|
||||
<label>Notes: <textarea name="notes"></textarea></label><br>
|
||||
|
|
|
@ -187,6 +187,10 @@ export default class Schedule {
|
|||
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() {
|
||||
return [...this.all_teachers.concat(this.all_non_teacher_staff)
|
||||
.map(name => ({name, kind: 'staff'})),
|
||||
|
@ -198,6 +202,7 @@ export default class Schedule {
|
|||
this.all_students.push(name);
|
||||
this.all_students.sort();
|
||||
this.notAssignedStudents.add(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
addStaff(name) {
|
||||
|
@ -205,6 +210,7 @@ export default class Schedule {
|
|||
this.all_staff.push(name);
|
||||
this.all_staff.sort();
|
||||
this.notAssignedStaff.add(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
addTeacher(name) {
|
||||
|
@ -212,6 +218,7 @@ export default class Schedule {
|
|||
this.all_teachers.push(name);
|
||||
this.all_teachers.sort();
|
||||
this.notAssignedStaff.add(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
delStaff(name) {
|
||||
|
|
|
@ -9,7 +9,8 @@ export default class ScheduleGrid {
|
|||
|
||||
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
|
||||
// actually multiple tracks, make as many events as possible full width.
|
||||
|
@ -50,22 +51,42 @@ export default class ScheduleGrid {
|
|||
eventsWithoutTracks.sort((a, b) => a.start_time.cmp(b.start_time));
|
||||
for (const event of eventsWithoutTracks) {
|
||||
// Put the event in the first track with space for it.
|
||||
tracks.forEach((track, trackNum) => {
|
||||
if (event.track === undefined) {
|
||||
const hasOverlappingEvents = track.some(e => e.at_same_time_as(event));
|
||||
if (!hasOverlappingEvents) {
|
||||
track.push(event);
|
||||
event.track = trackNum;
|
||||
const tracksWithSpace = Object.keys(tracks).map(t => parseInt(t)).filter(trackNum => {
|
||||
const end = Math.min(trackNum + event.width, tracks.length);
|
||||
for (let i = trackNum; i < end; i++) {
|
||||
if (tracks[i].some(e => e.at_same_time_as(event))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// No track had space for it, create a new track to put it in.
|
||||
if (event.track === undefined) {
|
||||
event.track = tracks.length;
|
||||
return true;
|
||||
})
|
||||
.map(trackNum => {
|
||||
const staffLists = [...tracks[trackNum], event]
|
||||
.map(e => e.assignment.people_by_day[e.day].staff)
|
||||
.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".
|
||||
// If another event happens at the same time, we give that event
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export class Time {
|
||||
constructor(obj) {
|
||||
this.hour = obj.hour ?? 0;
|
||||
this.minute = obj.minute ?? 0;
|
||||
this.second = obj.second ?? 0;
|
||||
this.hour = parseInt(obj.hour ?? 0);
|
||||
this.minute = parseInt(obj.minute ?? 0);
|
||||
this.second = parseInt(obj.second ?? 0);
|
||||
}
|
||||
|
||||
static fromInputValue(str) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user