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');
|
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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,22 +51,42 @@ 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)
|
||||||
|
.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]);
|
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".
|
||||||
// If another event happens at the same time, we give that event
|
// If another event happens at the same time, we give that event
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user