github-actions[bot]
chore: sync uc-lessons Space
4b94493
Raw
History Blame Contribute Delete
17.1 kB
/* views.js — Custom timeline views for Group, Room, and Teacher */
// biome-ignore-all lint: don't lint
var SLOT_MINUTES = 60;
// Mapping jours de la semaine
var DAY_MAP = {
Mon: 0, Monday: 0,
Tue: 1, Tuesday: 1,
Wed: 2, Wednesday: 2,
Thu: 3, Thursday: 3,
Fri: 4, Friday: 4,
Sat: 5, Saturday: 5,
Sun: 6, Sunday: 6,
};
var WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
var WEEKDAY_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Parse une heure au format "HH:MM:SS" ou "HH:MM" en minutes depuis minuit
function parseTimeToMinutes(timeStr) {
if (!timeStr) return 0;
var parts = timeStr.split(':');
var hours = parseInt(parts[0], 10) || 0;
var minutes = parseInt(parts[1], 10) || 0;
// Clamp to valid range (0-1439 for a single day)
return Math.max(0, Math.min(hours * 60 + minutes, 1439));
}
// Convertit un timeslot en minutes absolues (depuis Lundi 00:00)
function timeslotToMinutes(timeslot) {
if (!timeslot) return { startMinute: 0, endMinute: SLOT_MINUTES };
var dayIndex = DAY_MAP[timeslot.day_of_week];
if (dayIndex == null) dayIndex = 0;
var startMin = parseTimeToMinutes(timeslot.start_time);
var endMin = parseTimeToMinutes(timeslot.end_time);
// Garantir que endMinute > startMinute
if (endMin <= startMin) {
endMin = startMin + SLOT_MINUTES; // Durée par défaut de 60 minutes
}
return {
startMinute: dayIndex * 1440 + startMin,
endMinute: dayIndex * 1440 + endMin,
};
}
function formatClock(totalMinutes) {
var minutesInDay = ((totalMinutes % 1440) + 1440) % 1440;
var hours = Math.floor(minutesInDay / 60);
var minutes = minutesInDay % 60;
return String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0');
}
function weekdayIndex(day) {
var index = DAY_MAP[day];
return index == null ? 0 : index;
}
function weekdayShortLabel(day) {
return WEEKDAY_SHORT[weekdayIndex(day)] || String(day || 'Day');
}
function safeId(value) {
return String(value == null ? 'unknown' : value).replace(/[^A-Za-z0-9_-]+/g, '-');
}
function isAssignedIndex(index, collection) {
return Number.isInteger(index) && index >= 0 && index < collection.length;
}
function assignedFact(collection, index) {
return isAssignedIndex(index, collection) ? collection[index] : null;
}
function factLabel(fact, fallback) {
if (!fact) return fallback;
return fact.name || fact.id || fact.code || fallback;
}
function scheduledBadges(totalCount, scheduledCount) {
if (!totalCount) return ['No lessons'];
var complete = scheduledCount === totalCount;
return [{
label: scheduledCount + '/' + totalCount + ' scheduled',
style: complete ? {
bg: '#ecfdf5',
border: '1px solid #a7f3d0',
color: '#047857',
} : {
bg: '#fffbeb',
border: '1px solid #fde68a',
color: '#92400e',
},
}];
}
function countScheduled(lessons, timeslots) {
return lessons.reduce(function (count, lesson) {
return assignedFact(timeslots, lesson.timeslot_idx) ? count + 1 : count;
}, 0);
}
function teachingWindowLabel(timeslots) {
if (!timeslots || !timeslots.length) return 'No timetable';
var minStart = 1440;
var maxEnd = 0;
timeslots.forEach(function (timeslot) {
minStart = Math.min(minStart, parseTimeToMinutes(timeslot.start_time));
maxEnd = Math.max(maxEnd, parseTimeToMinutes(timeslot.end_time));
});
return 'Mon-Fri ' + formatClock(minStart) + '-' + formatClock(maxEnd);
}
function formatTimeslot(timeslot) {
if (!timeslot) return 'Timeslot unassigned';
return weekdayShortLabel(timeslot.day_of_week) + ' ' +
formatClock(parseTimeToMinutes(timeslot.start_time)) + '-' +
formatClock(parseTimeToMinutes(timeslot.end_time));
}
function buildLessonTimelineItem(prefix, lesson, lessonIndex, lookups, toneForKey, entityLabel) {
var timeslot = assignedFact(lookups.timeslots, lesson.timeslot_idx);
if (!timeslot) return null;
var room = assignedFact(lookups.rooms, lesson.room_idx);
var teacher = assignedFact(lookups.teachers, lesson.teacher_idx);
var group = assignedFact(lookups.groups, lesson.group_idx);
var tsMinutes = timeslotToMinutes(timeslot);
var subject = lesson.subject || entityLabel(lesson, lessonIndex);
return {
id: prefix + '-' + safeId(lesson.id || lessonIndex),
startMinute: tsMinutes.startMinute,
endMinute: tsMinutes.endMinute,
label: subject,
meta: [
{ label: 'Room', value: factLabel(room, 'Unassigned room') },
{ label: 'Teacher', value: factLabel(teacher, 'Unassigned teacher') },
{ label: 'Cohort', value: factLabel(group, 'Unassigned cohort') },
{ label: 'Period', value: formatTimeslot(timeslot) },
{ label: 'Students', value: String(lesson.student_count || '') },
],
tone: toneForKey(subject),
};
}
// Construire l'axe à partir des timeslots
function buildAxisFromTimeslots(timeslots) {
if (!timeslots || !timeslots.length) {
// Fallback : un seul jour de 8h à 18h
return {
startMinute: 0,
endMinute: 10 * 60, // 10 slots de 60 min
days: [{ id: 'day-0', label: 'Monday', subLabel: '08:00-18:00', startMinute: 0, endMinute: 1440, isWeekend: false }],
ticks: [],
initialViewport: { startMinute: 0, endMinute: 600 },
};
}
// Déterminer quels jours sont présents
var presentDays = [];
timeslots.forEach(function (ts) {
var day = ts.day_of_week;
if (day && presentDays.indexOf(day) === -1) {
presentDays.push(day);
}
});
presentDays.sort(function (a, b) { return DAY_MAP[a] - DAY_MAP[b]; });
var days = [];
var ticks = [];
var maxEndMinute = 0;
var minStartInDay = 1440;
var maxEndInDay = 0;
timeslots.forEach(function (ts) {
minStartInDay = Math.min(minStartInDay, parseTimeToMinutes(ts.start_time));
maxEndInDay = Math.max(maxEndInDay, parseTimeToMinutes(ts.end_time));
});
if (minStartInDay >= maxEndInDay) {
minStartInDay = 8 * 60;
maxEndInDay = 18 * 60;
}
// Créer les blocs "days" (un par jour)
presentDays.forEach(function (day) {
var dayIndex = DAY_MAP[day];
var dayStart = dayIndex * 1440 + minStartInDay;
var dayEnd = dayIndex * 1440 + maxEndInDay;
days.push({
id: 'day-' + day,
label: WEEKDAYS[dayIndex],
subLabel: formatClock(minStartInDay) + '-' + formatClock(maxEndInDay),
startMinute: dayStart,
endMinute: dayEnd,
isWeekend: day === 'Sat' || day === 'Sun',
});
});
// Créer les ticks horaires à partir de la fenêtre d'enseignement réelle.
presentDays.forEach(function (day) {
var dayIndex = DAY_MAP[day];
for (var h = Math.floor(minStartInDay / 60); h <= Math.ceil(maxEndInDay / 60); h += 2) {
ticks.push({
id: 'tick-' + day + '-h' + h,
minute: dayIndex * 1440 + h * 60,
label: h + 'h',
});
}
});
// Calculer la fin maximale à partir des timeslots
timeslots.forEach(function (ts) {
var end = DAY_MAP[ts.day_of_week] * 1440 + parseTimeToMinutes(ts.end_time);
maxEndMinute = Math.max(maxEndMinute, end);
});
// Si aucun timeslot n'a de jour valide, utiliser 5 jours par défaut
if (presentDays.length === 0) {
for (var d = 0; d < 5; d++) {
days.push({
id: 'day-' + d,
label: WEEKDAYS[d],
startMinute: d * 1440,
endMinute: (d + 1) * 1440,
isWeekend: false,
});
for (var h = 8; h <= 18; h += 2) {
ticks.push({
id: 'tick-day' + d + '-h' + h,
minute: d * 1440 + h * 60,
label: h + 'h',
});
}
}
maxEndMinute = 5 * 1440;
}
return {
startMinute: days.length ? days[0].startMinute : 0,
endMinute: maxEndMinute,
days: days,
ticks: ticks,
initialViewport: {
startMinute: days.length ? days[0].startMinute : 0,
endMinute: days.length ? Math.min(maxEndMinute, days[0].endMinute) : Math.min(maxEndMinute, 10 * 60),
},
};
}
function ensureCustomTimeline(key, customTimelines, SF, timelineConfig) {
var timeline = customTimelines[key];
if (!timeline) {
timeline = SF.rail.createTimeline(timelineConfig);
customTimelines[key] = timeline;
return timeline;
}
timeline.setModel(timelineConfig.model);
return timeline;
}
export function renderByGroup(data, container, SF, toneForKey, entityLabel, customTimelines) {
var lessons = data.lessons || [];
var groups = data.groups || [];
var timeslots = data.timeslots || [];
var rooms = data.rooms || [];
var teachers = data.teachers || [];
console.log('renderByGroup: groups=' + groups.length + ', lessons=' + lessons.length);
if (!lessons.length) {
container.innerHTML = '<p>No lessons available.</p>';
return;
}
// Créer une lane par groupe existant, même sans lessons
var byGroup = {};
groups.forEach(function(group, idx) {
var groupKey = group.name || 'Group ' + idx;
byGroup[groupKey] = { group: group, lessons: [] };
});
// Assigner les lessons aux groupes
lessons.forEach(function (lesson) {
var groupIdx = lesson.group_idx;
if (groupIdx == null || !groups[groupIdx]) {
// Lesson sans groupe : créer une lane "Unassigned"
var unassignedKey = 'Unassigned';
if (!byGroup[unassignedKey]) {
byGroup[unassignedKey] = { group: { name: unassignedKey }, lessons: [] };
}
byGroup[unassignedKey].lessons.push(lesson);
return;
}
var group = groups[groupIdx];
var groupKey = group.name || 'Group ' + groupIdx;
if (!byGroup[groupKey]) {
byGroup[groupKey] = { group: group, lessons: [] };
}
byGroup[groupKey].lessons.push(lesson);
});
var axis = buildAxisFromTimeslots(timeslots);
var lanes = Object.entries(byGroup).map(function (entry) {
var groupKey = entry[0];
var groupData = entry[1];
var scheduledCount = countScheduled(groupData.lessons, timeslots);
var items = groupData.lessons.map(function (lesson, idx) {
return buildLessonTimelineItem(
'group-' + safeId(groupKey),
lesson,
idx,
{ timeslots: timeslots, rooms: rooms, teachers: teachers, groups: groups },
toneForKey,
entityLabel
);
}).filter(Boolean);
return {
id: 'group-' + safeId(groupKey),
label: groupKey + (groupData.group.code ? ' (' + groupData.group.code + ')' : ''),
mode: 'detailed',
badges: scheduledBadges(groupData.lessons.length, scheduledCount),
stats: [],
items: items,
};
});
var timeline = ensureCustomTimeline('by-group', customTimelines, SF, {
label: 'Groups',
labelWidth: 240,
title: 'Cohort Timetables',
subtitle: 'Fixed Mon-Fri teaching week',
zoomPresets: [],
model: { axis: axis, lanes: lanes },
});
container.innerHTML = '';
var realGroupCount = Object.keys(byGroup).filter(function(key) { return key !== 'Unassigned'; }).length;
container.appendChild(SF.createTable({
columns: ['Cohorts', 'Lessons', 'Scheduled', 'Window'],
rows: [[String(realGroupCount), String(lessons.length), String(countScheduled(lessons, timeslots)), teachingWindowLabel(timeslots)]],
}));
container.appendChild(timeline.el);
}
export function renderByRoom(data, container, SF, toneForKey, entityLabel, customTimelines) {
var lessons = data.lessons || [];
var rooms = data.rooms || [];
var timeslots = data.timeslots || [];
var groups = data.groups || [];
var teachers = data.teachers || [];
console.log('renderByRoom: rooms=' + rooms.length + ', lessons=' + lessons.length);
if (!lessons.length) {
container.innerHTML = '<p>No lessons available.</p>';
return;
}
// Créer une lane par room existant, même sans lessons
var byRoom = {};
rooms.forEach(function(room, idx) {
var roomKey = room.name || 'Room ' + idx;
byRoom[roomKey] = { room: room, lessons: [] };
});
// Assigner les lessons aux rooms
lessons.forEach(function (lesson) {
var roomIdx = lesson.room_idx;
if (!assignedFact(rooms, roomIdx)) {
var unassignedKey = 'Unassigned room';
if (!byRoom[unassignedKey]) {
byRoom[unassignedKey] = { room: { name: unassignedKey }, lessons: [] };
}
byRoom[unassignedKey].lessons.push(lesson);
return;
}
var room = rooms[roomIdx];
var roomKey = room.name || 'Room ' + roomIdx;
if (!byRoom[roomKey]) {
byRoom[roomKey] = { room: room, lessons: [] };
}
byRoom[roomKey].lessons.push(lesson);
});
var axis = buildAxisFromTimeslots(timeslots);
var lanes = Object.entries(byRoom).map(function (entry) {
var roomKey = entry[0];
var roomData = entry[1];
var scheduledCount = countScheduled(roomData.lessons, timeslots);
var items = roomData.lessons.map(function (lesson, idx) {
return buildLessonTimelineItem(
'room-' + safeId(roomKey),
lesson,
idx,
{ timeslots: timeslots, rooms: rooms, teachers: teachers, groups: groups },
toneForKey,
entityLabel
);
}).filter(Boolean);
return {
id: 'room-' + safeId(roomKey),
label: roomKey + (roomData.room.code ? ' (' + roomData.room.code + ')' : ''),
mode: 'detailed',
badges: roomData.lessons.length === 0 ? ['Empty'] : scheduledBadges(roomData.lessons.length, scheduledCount),
stats: [],
items: items,
};
});
var timeline = ensureCustomTimeline('by-room', customTimelines, SF, {
label: 'Rooms',
labelWidth: 240,
title: 'Room Utilization',
subtitle: 'Fixed Mon-Fri teaching week',
zoomPresets: [],
model: { axis: axis, lanes: lanes },
});
container.innerHTML = '';
var realRoomCount = Object.keys(byRoom).filter(function(key) { return key !== 'Unassigned room'; }).length;
container.appendChild(SF.createTable({
columns: ['Rooms', 'Lessons', 'Scheduled', 'Window'],
rows: [[String(realRoomCount), String(lessons.length), String(countScheduled(lessons, timeslots)), teachingWindowLabel(timeslots)]],
}));
container.appendChild(timeline.el);
}
export function renderByTeacher(data, container, SF, toneForKey, entityLabel, customTimelines) {
var lessons = data.lessons || [];
var teachers = data.teachers || [];
var timeslots = data.timeslots || [];
var rooms = data.rooms || [];
var groups = data.groups || [];
console.log('renderByTeacher: teachers=' + teachers.length + ', lessons=' + lessons.length);
if (!lessons.length) {
container.innerHTML = '<p>No lessons available.</p>';
return;
}
// Créer une lane par teacher existant, même sans lessons
var byTeacher = {};
teachers.forEach(function(teacher, idx) {
var teacherKey = teacher.name || 'Teacher ' + idx;
byTeacher[teacherKey] = { teacher: teacher, lessons: [] };
});
// Assigner les lessons aux teachers
lessons.forEach(function (lesson) {
var teacherIdx = lesson.teacher_idx;
if (teacherIdx == null || !teachers[teacherIdx]) {
// Lesson sans teacher : créer une lane "Unassigned"
var unassignedKey = 'Unassigned';
if (!byTeacher[unassignedKey]) {
byTeacher[unassignedKey] = { teacher: { name: unassignedKey }, lessons: [] };
}
byTeacher[unassignedKey].lessons.push(lesson);
return;
}
var teacher = teachers[teacherIdx];
var teacherKey = teacher.name || 'Teacher ' + teacherIdx;
if (!byTeacher[teacherKey]) {
byTeacher[teacherKey] = { teacher: teacher, lessons: [] };
}
byTeacher[teacherKey].lessons.push(lesson);
});
var axis = buildAxisFromTimeslots(timeslots);
var lanes = Object.entries(byTeacher).map(function (entry) {
var teacherKey = entry[0];
var teacherData = entry[1];
var scheduledCount = countScheduled(teacherData.lessons, timeslots);
var items = teacherData.lessons.map(function (lesson, idx) {
return buildLessonTimelineItem(
'teacher-' + safeId(teacherKey),
lesson,
idx,
{ timeslots: timeslots, rooms: rooms, teachers: teachers, groups: groups },
toneForKey,
entityLabel
);
}).filter(Boolean);
return {
id: 'teacher-' + safeId(teacherKey),
label: teacherKey + (teacherData.teacher.code ? ' (' + teacherData.teacher.code + ')' : ''),
mode: 'detailed',
badges: teacherData.lessons.length === 0 ? ['Empty'] : scheduledBadges(teacherData.lessons.length, scheduledCount),
stats: [],
items: items,
};
});
var timeline = ensureCustomTimeline('by-teacher', customTimelines, SF, {
label: 'Teachers',
labelWidth: 240,
title: 'Teacher Loads',
subtitle: 'Fixed Mon-Fri teaching week',
zoomPresets: [],
model: { axis: axis, lanes: lanes },
});
container.innerHTML = '';
var realTeacherCount = Object.keys(byTeacher).filter(function(key) { return key !== 'Unassigned'; }).length;
container.appendChild(SF.createTable({
columns: ['Teachers', 'Lessons', 'Scheduled', 'Window'],
rows: [[String(realTeacherCount), String(lessons.length), String(countScheduled(lessons, timeslots)), teachingWindowLabel(timeslots)]],
}));
container.appendChild(timeline.el);
}