Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ coverage/
# OS
.DS_Store
Thumbs.db

# Claude Code local tooling
.claude/
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ A front-end only web application for scheduling instructor workloads with drag-a

### Course Management
Courses should be treated as an instance of a section.
- Add courses with name, credits, and assigned instructor
- Add courses with name, credits, and assigned instructor(s)
- Credits may be fractional (e.g. 2.5)
- **Co-teaching** — assign multiple instructors to a course and split its credits between them. Each instructor's workload counts only their share, and the split must add up to the course's total credits
- Edit course details by **double-clicking** on any scheduled course
- Delete courses (automatically removes from schedule)
- Drag courses to schedule slots
Expand Down
16 changes: 9 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ <h2 class="collapsible-header" data-toggle-section="courses">
</select>
<input type="text" id="courseNumber" placeholder="Course number (e.g. 101)">
<input type="text" id="courseName" placeholder="Course name (optional)">
<input type="number" id="courseCredits" placeholder="Credits" min="1" required>
<input type="number" id="courseCredits" placeholder="Credits" min="0.5" step="0.5" required>
<input type="text" id="courseQuarterTaken" placeholder="Cohort (e.g. Q1, Q2)">
<div class="quarters-offered-dropdown">
<button type="button" class="filter-toggle" data-toggle-quarters="add">Quarters Offered ▼</button>
Expand Down Expand Up @@ -169,7 +169,7 @@ <h2>Edit Course</h2>
</div>
<div class="form-group">
<label for="editCourseCredits">Credits</label>
<input type="number" id="editCourseCredits" min="1" required>
<input type="number" id="editCourseCredits" min="0.5" step="0.5" data-action="course-credits-change" required>
</div>
<div class="form-group">
<label for="editCourseQuarterTaken">Cohort</label>
Expand All @@ -196,10 +196,11 @@ <h2>Edit Course</h2>
</div>
</div>
<div class="form-group">
<label for="editCourseInstructor">Instructor (this section)</label>
<select id="editCourseInstructor">
<option value="">Select Instructor (Optional)</option>
</select>
<label>Instructors &amp; Credit Split (this section)</label>
<div id="editInstructorRows" class="instructor-rows"></div>
<button type="button" class="btn-secondary add-instructor-row-btn"
data-action="add-instructor-row">+ Add Instructor</button>
<div id="instructorCreditsSummary" class="credits-summary"></div>
</div>
<div class="form-group">
<label for="editCourseSection">Section</label>
Expand Down Expand Up @@ -291,7 +292,7 @@ <h2>How to Use</h2>
<li><strong>Add Programs</strong> - Create programs (e.g. CPW) to organize courses</li>
<li><strong>Add Instructors</strong> - Enter names in the Instructors panel on the left</li>
<li><strong>Add Courses</strong> - Add courses to the global catalog with program, number, and credits</li>
<li><strong>Assign Instructors</strong> - Double-click a course to assign an instructor for the current schedule</li>
<li><strong>Assign Instructors</strong> - Double-click a course to assign one or more instructors for the current schedule, splitting the course's credits between them</li>
<li><strong>Add Classrooms</strong> - Create rooms with room numbers</li>
<li><strong>Add Time Slots</strong> - Set up shared schedule times for each day (applies to all rooms)</li>
<li><strong>Set Quarter</strong> - Choose a quarter (Fall/Winter/Spring/Summer) for each schedule</li>
Expand All @@ -306,6 +307,7 @@ <h2>Tips</h2>
<ul style="line-height: 1.8; margin: 20px 0;">
<li>Courses are stored in a <strong>global catalog</strong> shared across all schedules</li>
<li><strong>Instructor assignments</strong> are per-schedule and per-section — the same course can have different instructors in different sections and schedules</li>
<li><strong>Co-teaching</strong> - Add multiple instructors to a course and split its credits between them; each instructor's workload counts only their share. Credits can be fractional (e.g. 1.5)</li>
<li><strong>Instructors</strong> are stored globally and shared across all schedules, so you only need to add them once</li>
<li>Set <strong>Quarters Offered</strong> on courses to control which quarters they can be scheduled in</li>
<li>Courses with no quarters offered are treated as <strong>"on demand"</strong> and available any quarter</li>
Expand Down
11 changes: 9 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,14 @@ class App {
const courseNumber = document.getElementById('editCourseNumber').value.trim() || '';
const name = document.getElementById('editCourseName').value.trim();
const credits = document.getElementById('editCourseCredits').value;
const instructorId = document.getElementById('editCourseInstructor').value;
const modality = document.getElementById('editModality').value || 'in-person';
const quarterTaken = document.getElementById('editCourseQuarterTaken')?.value.trim() || null;
const quartersOffered = this.#uiState.getQuartersOfferedFromCheckboxes('edit');
const section = document.getElementById('editCourseSection')?.value.trim() || '';

if (credits) {
this.#modals.saveCourseChanges(
courseId, name, credits, instructorId || null,
courseId, name, credits,
classroomId, day, timeslot, modality, courseIndex,
quarterTaken, programId, courseNumber, quartersOffered, section
);
Expand Down Expand Up @@ -358,6 +357,12 @@ class App {
this.#uiState.updateInstructorFilter(instrId, btn.checked);
break;
}
case 'add-instructor-row':
this.#modals.addInstructorRow();
break;
case 'remove-instructor-row':
this.#modals.removeInstructorRow(btn.closest('.instructor-row'));
break;
}
}

Expand Down Expand Up @@ -434,6 +439,8 @@ class App {
} else if (action === 'timeslot-edit-end') {
const { day, ts, start } = el.dataset;
this.#timeslotManager.edit(day, ts, start, el.value);
} else if (action === 'instructor-row-change' || action === 'course-credits-change') {
this.#modals.updateCreditsSummary();
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/data/DataStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,22 @@ export class DataStore {
if (sd.quarter === undefined) sd.quarter = '';
delete sd.instructors;

// Co-teaching migration: a legacy assignment was a bare instructor-id
// string. Convert to an array of { instructorId, credits } entries, the
// lone instructor taking the course's full credit total.
Object.keys(sd.courseInstructors).forEach(key => {
const value = sd.courseInstructors[key];
if (Array.isArray(value)) return;
if (typeof value === 'string' && value) {
const courseId = key.includes('::') ? key.split('::')[0] : key;
const course = this.#data.courseCatalog.find(c => c.id === courseId);
const credits = course ? course.credits : null;
sd.courseInstructors[key] = [{ instructorId: value, credits }];
} else {
delete sd.courseInstructors[key];
}
});

// Old per-schedule courses array → global catalog
if (sd.courses) {
sd.courses.forEach(course => {
Expand Down
4 changes: 2 additions & 2 deletions src/managers/CourseManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export class CourseManager {
const creditsInput = document.getElementById('courseCredits');
const quarterInput = document.getElementById('courseQuarterTaken');

const credits = parseInt(creditsInput.value);
if (!credits) return;
const credits = parseFloat(creditsInput.value);
if (!credits || credits <= 0) return;

const quartersOffered = this.#uiState.getQuartersOfferedFromCheckboxes('add');

Expand Down
26 changes: 16 additions & 10 deletions src/managers/InstructorManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ export class InstructorManager {
// Check ALL schedules for assignments (instructors are global)
for (const [scheduleName, sd] of Object.entries(d.schedules)) {
if (sd.courseInstructors) {
const hasAssignments = Object.values(sd.courseInstructors).some(iId => iId === id);
const hasAssignments = Object.values(sd.courseInstructors).some(value =>
Helpers.normalizeInstructorAssignments(value).some(a => a.instructorId === id)
);
if (hasAssignments) {
alert(`Cannot delete instructor with assigned courses in schedule "${scheduleName}". Unassign them first.`);
return;
Expand All @@ -51,27 +53,31 @@ export class InstructorManager {
const d = this.#dataStore.data;
const sd = this.#dataStore.getCurrentSchedule();

// Collect all unique (courseId, section) keys that are in the grid
const scheduledPairs = new Set();
// Collect all unique (courseId, section) pairs that are in the grid.
const scheduledPairs = new Map(); // key -> { courseId, section }
for (const classroomId in d.schedule) {
for (const day in d.schedule[classroomId]) {
for (const time in d.schedule[classroomId][day]) {
const slotData = d.schedule[classroomId][day][time];
const items = Array.isArray(slotData) ? slotData : (slotData ? [slotData] : []);
items.forEach(item => {
scheduledPairs.add(Helpers.getCourseInstructorKey(item.courseId, item.section));
const section = item.section || '';
const key = Helpers.getCourseInstructorKey(item.courseId, section);
scheduledPairs.set(key, { courseId: item.courseId, section });
});
}
}
}

// Sum credits for unique pairs assigned to this instructor
// Sum this instructor's credit share across each unique scheduled pair.
let total = 0;
scheduledPairs.forEach(key => {
if (sd.courseInstructors[key] === instructorId) {
const courseId = key.includes('::') ? key.split('::')[0] : key;
const course = d.courseCatalog.find(c => c.id === courseId);
if (course) total += course.credits;
scheduledPairs.forEach(({ courseId, section }) => {
const course = d.courseCatalog.find(c => c.id === courseId);
if (!course) return;
const assignment = Helpers.getInstructorAssignments(sd, courseId, section)
.find(a => a.instructorId === instructorId);
if (assignment) {
total += assignment.credits == null ? course.credits : assignment.credits;
}
});
return total;
Expand Down
4 changes: 2 additions & 2 deletions src/ui/CohortSummaryView.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class CohortSummaryView {
return `
<div class="cohort-class">
<div class="cohort-class-name">${cls.name}</div>
<div class="cohort-class-info">${cls.credits} credits</div>
<div class="cohort-class-info">${Helpers.formatCredits(cls.credits)} credits</div>
<div class="cohort-schedule-list">${scheduleHTML}</div>
</div>
`;
Expand All @@ -138,7 +138,7 @@ export class CohortSummaryView {
<h3 class="cohort-name">Cohort: ${cohort.name}</h3>
<div class="cohort-stats">
<span class="cohort-stat"><strong>Days per week:</strong> ${daysPerWeek > 0 ? daysPerWeek : 'N/A'}</span>
<span class="cohort-stat"><strong>Total credits:</strong> ${totalCredits}</span>
<span class="cohort-stat"><strong>Total credits:</strong> ${Helpers.formatCredits(totalCredits)}</span>
</div>
<div class="cohort-classes-list">${classesHTML}</div>
</div>
Expand Down
Loading
Loading