Saaas007/room-utilization-system
0
1(() => {2 // Utility helpers3 const $ = (sel, ctx = document) => ctx.querySelector(sel);4 const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));5 const pad = (n) => String(n).padStart(2, '0');6 const toMinutes = (hhmm) => {7 const [h, m] = hhmm.split(':').map(Number);8 return h * 60 + m;9 };10 const nowMinutes = () => {11 const d = new Date();12 return d.getHours() * 60 + d.getMinutes();13 };14 const todayIndex = () => {15 // Sunday=0..Saturday=6; JS default16 return new Date().getDay();17 };18 const formatTime = (hhmm) => {19 // Display in 12-hour format20 let [h, m] = hhmm.split(':').map(Number);21 const ampm = h >= 12 ? 'PM' : 'AM';22 h = h % 12; h = h === 0 ? 12 : h;23 return `${h}:${pad(m)} ${ampm}`;24 };25 const dayShort = (idx) => ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][idx];26 27 // Data store28 const STORAGE_KEY = 'srud_data_v1';29 const defaultSections = ['BSIT NS', 'WMAD', 'BPO'];30 let state = {31 rooms: [],32 sections: [...defaultSections],33 schedules: [], // {id, roomId, section, instructor, days:[0..6], start:'HH:MM', end:'HH:MM'}34 };35 36 function save() {37 localStorage.setItem(STORAGE_KEY, JSON.stringify(state));38 }39 function load() {40 const raw = localStorage.getItem(STORAGE_KEY);41 if (raw) {42 try {43 const parsed = JSON.parse(raw);44 if (parsed && parsed.rooms && parsed.schedules && parsed.sections) {45 state = parsed;46 }47 } catch (e) {48 console.warn('Failed to parse stored data, using defaults.', e);49 }50 }51 if (!state.rooms.length) {52 seed();53 save();54 }55 }56 function seed() {57 state.rooms = [58 { id: uid(), name: 'ROOM 101', capacity: 40, building: 'Main' },59 { id: uid(), name: 'LAB 1', capacity: 30, building: 'ICT' },60 { id: uid(), name: 'AUDITORIUM', capacity: 200, building: 'Main' },61 { id: uid(), name: 'ROOM 202', capacity: 35, building: 'West' },62 ];63 state.schedules = [64 // Room 10165 {66 id: uid(), roomId: state.rooms[0].id, section: 'WMAD', instructor: 'Mr. King',67 days: [1,3], start: '09:00', end: '11:00'68 },69 {70 id: uid(), roomId: state.rooms[0].id, section: 'BSIT NS', instructor: 'Ms. Lee',71 days: [2,4], start: '13:00', end: '15:30'72 },73 // LAB 174 {75 id: uid(), roomId: state.rooms[1].id, section: 'BPO', instructor: 'Ms. Cruz',76 days: [1,3,5], start: '08:00', end: '10:00'77 },78 {79 id: uid(), roomId: state.rooms[1].id, section: 'BSIT NS', instructor: 'Dr. Tan',80 days: [2], start: '14:00', end: '17:00'81 },82 // AUDITORIUM83 {84 id: uid(), roomId: state.rooms[2].id, section: 'BSIT NS', instructor: 'Prof. Diaz',85 days: [0], start: '09:00', end: '12:00'86 },87 {88 id: uid(), roomId: state.rooms[2].id, section: 'WMAD', instructor: 'Mr. Stark',89 days: [5], start: '15:00', end: '18:00'90 },91 // ROOM 20292 {93 id: uid(), roomId: state.rooms[3].id, section: 'BPO', instructor: 'Ms. Garcia',94 days: [4], start: '10:00', end: '12:00'95 },96 ];97 }98 function uid() {99 return Math.random().toString(36).slice(2, 10);100 }101 102 // Core business logic103 function getRoomById(id) {104 return state.rooms.find(r => r.id === id);105 }106 function getSectionSchedules(roomId, section) {107 return state.schedules.filter(s => s.roomId === roomId && s.section === section);108 }109 function isNowActive(schedule) {110 // Active if day matches today and current time between start/end111 const now = new Date();112 const dayIdx = now.getDay();113 if (!schedule.days.includes(dayIdx)) return false;114 const cur = now.getHours() * 60 + now.getMinutes();115 const [sh, sm] = schedule.start.split(':').map(Number);116 const [eh, em] = schedule.end.split(':').map(Number);117 const startMin = sh * 60 + sm;118 const endMin = eh * 60 + em;119 return cur >= startMin && cur <= endMin;120 }121 function getCurrentSection(roomId) {122 const schedules = state.schedules.filter(s => s.roomId === roomId && isNowActive(s));123 if (schedules.length === 0) return null;124 // Choose the one ending the soonest125 schedules.sort((a,b) => toMinutes(a.end) - toMinutes(b.end));126 return schedules[0];127 }128 function getNextSchedule(roomId) {129 const now = new Date();130 let best = null;131 let bestDelta = Infinity;132 const today = now.getDay();133 const curMin = now.getHours() * 60 + now.getMinutes();134 135 const schedules = state.schedules.filter(s => s.roomId === roomId);136 schedules.forEach(s => {137 const startMin = toMinutes(s.start);138 const endMin = toMinutes(s.end);139 140 // If today has remaining time141 if (s.days.includes(today) && curMin < endMin) {142 const delta = startMin - curMin;143 if (delta >= 0 && delta < bestDelta) {144 bestDelta = delta;145 best = s;146 }147 }148 // Find next day in week149 for (let i = 1; i <= 7; i++) {150 const day = (today + i) % 7;151 if (s.days.includes(day)) {152 const delta = (24*60 - curMin) + startMin + (i-1)*24*60;153 if (delta < bestDelta) {154 bestDelta = delta;155 best = s;156 }157 break;158 }159 }160 });161 return best;162 }163 function computeRoomOccupancy(roomId) {164 const curr = getCurrentSection(roomId);165 const next = curr ? getNextSchedule(roomId) : getNextSchedule(roomId);166 let status = 'Available';167 let statusColor = 'text-success';168 if (curr) {169 status = `In use: ${curr.section}`;170 statusColor = 'text-danger';171 } else if (next) {172 status = `Next: ${next.section} (${formatTime(next.start)})`;173 statusColor = 'text-warning';174 }175 const upcoming = state.schedules176 .filter(s => s.roomId === roomId)177 .sort((a,b) => toMinutes(a.start) - toMinutes(b.start));178 return { current: curr, next, status, statusColor, upcoming };179 }180 // UI rendering181 const roomList = $('#room-list');182 function renderFilters() {183 const sel = $('#filter-section');184 sel.innerHTML = `<option value="">All sections</option>` + state.sections185 .map(s => `<option value="${s}">${s}</option>`).join('');186 187 // Graph filters188 const graphRoomSel = $('#filter-graph-room');189 graphRoomSel.innerHTML = '<option value="">All Rooms</option>' + state.rooms190 .map(r => `<option value="${r.id}">${r.name}</option>`).join('');191 192 const graphSectionSel = $('#filter-graph-section');193 graphSectionSel.innerHTML = '<option value="">All Sections</option>' + state.sections194 .map(s => `<option value="${s}">${s}</option>`).join('');195 }196 function renderRooms() {197 const q = $('#search-room').value.trim().toLowerCase();198 const filterSec = $('#filter-section').value;199 const onlyAvailable = $('#toggle-available').checked;200let rooms = [...state.rooms];201 if (q) {202 rooms = rooms.filter(r => r.name.toLowerCase().includes(q) || (r.building || '').toLowerCase().includes(q));203 }204 if (filterSec) {205 const roomIds = new Set(state.schedules.filter(s => s.section === filterSec).map(s => s.roomId));206 rooms = rooms.filter(r => roomIds.has(r.id));207 }208 if (onlyAvailable) {209 rooms = rooms.filter(r => {210 const occ = computeRoomOccupancy(r.id);211 return occ.status.startsWith('Available');212 });213 }214 215 roomList.innerHTML = rooms.map(room => {216 const occ = computeRoomOccupancy(room.id);217 const currentInfo = occ.current218 ? `<div class="text-xs text-neutral-600">Now: <span class="font-medium">${occ.current.section}</span> until ${formatTime(occ.current.end)}</div>`219 : `<div class="text-xs text-neutral-500">No ongoing class</div>`;220 221 const statusBadge = `<span class="text-xs px-2 py-1 rounded ${occ.status.startsWith('Available') ? 'bg-success/10 text-success' : occ.status.startsWith('Next') ? 'bg-warning/10 text-warning' : 'bg-danger/10 text-danger'}">${occ.status}</span>`;222 223 return `224 <div class="rounded-xl border border-neutral-200 bg-white shadow-sm overflow-hidden hover:shadow-md transition">225 <div class="p-4">226 <div class="flex items-start justify-between">227 <div>228 <div class="text-lg font-semibold">${room.name}</div>229 <div class="text-xs text-neutral-500">${room.building || '—'} • Capacity ${room.capacity}</div>230 </div>231 ${statusBadge}232 </div>233 <div class="mt-3">234 ${currentInfo}235 </div>236 </div>237 <div class="px-4 py-2 bg-neutral-50 border-t border-neutral-200 flex items-center justify-between">238 <button class="view-detail text-primary-600 hover:underline text-sm" data-id="${room.id}">View details</button>239 <div class="flex items-center gap-2">240 <button class="px-2 py-1 text-xs rounded border border-neutral-300 hover:bg-neutral-100 quick-add" data-id="${room.id}">Add schedule</button>241 <button class="px-2 py-1 text-xs rounded border border-danger-300 text-danger hover:bg-danger-50 edit-room" data-id="${room.id}">Edit</button>242 <button class="px-2 py-1 text-xs rounded border border-neutral-300 hover:bg-neutral-100 delete-room" data-id="${room.id}">Delete</button>243 </div>244 </div>245 </div>`;246 }).join('') || `<div class="text-center text-neutral-500 border border-dashed border-neutral-300 rounded-xl p-8">No rooms match the current filters.</div>`;247 248 // Bind actions249 $$('.view-detail').forEach(btn => btn.addEventListener('click', () => openDetail(btn.dataset.id)));250 $$('.quick-add').forEach(btn => btn.addEventListener('click', () => {251 openScheduleModal(btn.dataset.id);252 }));253 $$('.edit-room').forEach(btn => btn.addEventListener('click', () => openEditRoom(btn.dataset.id)));254 $$('.delete-room').forEach(btn => btn.addEventListener('click', () => deleteRoom(btn.dataset.id)));255 }256 257 // Modals258 const overlay = $('#modal-overlay');259 function openModal(id) {260 overlay.classList.remove('hidden');261 $('#' + id).classList.remove('hidden');262 }263 function closeModal(id) {264 $('#' + id).classList.add('hidden');265 // If no modals visible, hide overlay266 const visible = $$('#modal-overlay > div').some(el => !el.classList.contains('hidden'));267 if (!visible) overlay.classList.add('hidden');268 }269 $$('[data-close]').forEach(btn => btn.addEventListener('click', () => closeModal(btn.getAttribute('data-close'))));270 overlay.addEventListener('click', (e) => {271 if (e.target === overlay) {272 $$('#modal-overlay > div').forEach(el => el.classList.add('hidden'));273 overlay.classList.add('hidden');274 }275 });276 277 // Room modal (create)278 const roomModal = $('#room-modal');279 const roomForm = $('#form-room');280 let editingRoomId = null;281 282 function openAddRoom() {283 editingRoomId = null;284 roomModal.querySelector('h3').textContent = 'Add Room';285 roomForm.reset();286 openModal('room-modal');287 }288 function openEditRoom(roomId) {289 editingRoomId = roomId;290 const room = getRoomById(roomId);291 roomModal.querySelector('h3').textContent = 'Edit Room';292 $('#room-name').value = room.name;293 $('#room-capacity').value = room.capacity;294 $('#room-building').value = room.building || '';295 openModal('room-modal');296 }297 function deleteRoom(roomId) {298 const room = getRoomById(roomId);299 if (!room) return;300 const schedules = state.schedules.filter(s => s.roomId === roomId);301 if (!confirm(`Delete room "${room.name}" and ${schedules.length} associated schedule(s)?`)) return;302 state.rooms = state.rooms.filter(r => r.id !== roomId);303 state.schedules = state.schedules.filter(s => s.roomId !== roomId);304 save();305 renderFilters();306 renderRooms();307 toast('Room deleted', 'success');308 }309 roomForm.addEventListener('submit', (e) => {310 e.preventDefault();311 const name = $('#room-name').value.trim();312 const capacity = parseInt($('#room-capacity').value, 10);313 const building = $('#room-building').value.trim();314 315 if (!name || isNaN(capacity)) return;316 317 if (editingRoomId) {318 const room = getRoomById(editingRoomId);319 room.name = name;320 room.capacity = capacity;321 room.building = building;322 toast('Room updated', 'success');323 } else {324 state.rooms.push({ id: uid(), name, capacity, building });325 toast('Room added', 'success');326 }327 save();328 closeModal('room-modal');329 renderFilters();330 renderRooms();331 });332 333 // Schedule modal334 const scheduleModal = $('#schedule-modal');335 const scheduleForm = $('#form-schedule');336 let scheduleTargetRoomId = null;337 338 function setupDaysCheckboxes() {339 const container = $('#schedule-days');340 const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];341 container.innerHTML = days.map((d, idx) => {342 return `<label class="day-chip cursor-pointer">343 <input type="checkbox" class="hidden" value="${idx}" />344 <span>${d}</span>345 </label>`;346 }).join('');347 // Toggle active style348 container.addEventListener('change', (e) => {349 if (e.target.type === 'checkbox') {350 e.target.parentElement.classList.toggle('active', e.target.checked);351 }352 });353 }354 function getSelectedDays() {355 return $$('#schedule-days input[type="checkbox"]:checked').map(cb => parseInt(cb.value, 10)).sort((a,b) => a-b);356 }357 function setSelectedDays(days) {358 $$('#schedule-days input[type="checkbox"]').forEach(cb => {359 const v = parseInt(cb.value, 10);360 cb.checked = days.includes(v);361 cb.parentElement.classList.toggle('active', cb.checked);362 });363 }364 function populateScheduleFormOptions() {365 const roomSel = $('#schedule-room');366 const secSel = $('#schedule-section');367 roomSel.innerHTML = state.rooms.map(r => `<option value="${r.id}">${r.name}</option>`).join('');368 secSel.innerHTML = state.sections.map(s => `<option value="${s}">${s}</option>`).join('');369 if (scheduleTargetRoomId) {370 roomSel.value = scheduleTargetRoomId;371 }372 }373 function openScheduleModal(roomId = null) {374 scheduleTargetRoomId = roomId;375 scheduleForm.reset();376 setSelectedDays([]);377 populateScheduleFormOptions();378 scheduleModal.querySelector('h3').textContent = 'Add Schedule';379 openModal('schedule-modal');380 }381 function validateScheduleTime(start, end) {382 return toMinutes(end) > toMinutes(start);383 }384 function hasConflict(roomId, section, days, start, end, ignoreId = null) {385 return state.schedules.some(s => {386 if (ignoreId && s.id === ignoreId) return false;387 if (roomId && s.roomId !== roomId) return false;388 if (section && s.section !== section) return false;389 if (!days.some(d => s.days.includes(d))) return false; // any overlapping day390 // Time overlap check391 const aStart = toMinutes(start), aEnd = toMinutes(end);392 const bStart = toMinutes(s.start), bEnd = toMinutes(s.end);393 return aStart < bEnd && bStart < aEnd;394 });395 }396 let editingScheduleId = null;397 398 scheduleForm.addEventListener('submit', (e) => {399 e.preventDefault();400 const roomId = $('#schedule-room').value;401 const section = $('#schedule-section').value;402 const instructor = $('#schedule-instructor').value.trim();403 const days = getSelectedDays();404 const start = $('#schedule-start').value;405 const end = $('#schedule-end').value;406 407 if (!roomId || !section || !instructor || !start || !end || days.length === 0) {408 toast('Please complete all fields and select at least one day.', 'error');409 return;410 }411 if (!validateScheduleTime(start, end)) {412 toast('End time must be after start time.', 'error');413 return;414 }415 if (hasConflict(roomId, section, days, start, end, editingScheduleId)) {416 toast('Time conflict detected for the selected room/section.', 'error');417 return;418 }419 420 if (editingScheduleId) {421 const sched = state.schedules.find(s => s.id === editingScheduleId);422 sched.roomId = roomId;423 sched.section = section;424 sched.instructor = instructor;425 sched.days = days;426 sched.start = start;427 sched.end = end;428 toast('Schedule updated', 'success');429 } else {430 state.schedules.push({431 id: uid(), roomId, section, instructor, days, start, end432 });433 toast('Schedule added', 'success');434 }435 save();436 closeModal('schedule-modal');437 renderRooms();438 });439 440 // Detail modal441 function openDetail(roomId) {442 const room = getRoomById(roomId);443 if (!room) return;444 $('#detail-title').textContent = `Room ${room.name}`;445 $('#detail-meta').textContent = `${room.building || '—'} • Capacity ${room.capacity}`;446 const tbody = $('#detail-schedules');447 const rows = state.schedules448 .filter(s => s.roomId === roomId)449 .sort((a,b) => toMinutes(a.start) - toMinutes(b.start))450 .map(s => {451 const active = isNowActive(s);452 const status = active ? '<span class="text-success text-xs">In Progress</span>' : `<span class="text-neutral-500 text-xs">Starts ${formatTime(s.start)}</span>`;453 const days = s.days.map(d => dayShort(d)).join(', ');454 return `455 <tr class="border-b border-neutral-100">456 <td class="py-2 pr-4">${s.section}</td>457 <td class="py-2 pr-4">${s.instructor}</td>458 <td class="py-2 pr-4">${days}</td>459 <td class="py-2 pr-4">${formatTime(s.start)} - ${formatTime(s.end)}</td>460 <td class="py-2 pr-4">${status}</td>461 <td class="py-2 pr-4 text-right">462 <button class="text-primary-600 hover:underline text-sm edit-sched" data-id="${s.id}">Edit</button>463 <button class="text-danger hover:underline text-sm ml-2 delete-sched" data-id="${s.id}">Delete</button>464 </td>465 </tr>466 `;467 }).join('');468 tbody.innerHTML = rows || `<tr><td colspan="6" class="py-3 text-center text-neutral-500">No schedules found.</td></tr>`;469 // Bind edit/delete470 $$('.edit-sched').forEach(btn => btn.addEventListener('click', () => {471 const sched = state.schedules.find(s => s.id === btn.dataset.id);472 if (!sched) return;473 editingScheduleId = sched.id;474 scheduleModal.querySelector('h3').textContent = 'Edit Schedule';475 populateScheduleFormOptions();476 $('#schedule-room').value = sched.roomId;477 $('#schedule-section').value = sched.section;478 $('#schedule-instructor').value = sched.instructor;479 setSelectedDays(sched.days);480 $('#schedule-start').value = sched.start;481 $('#schedule-end').value = sched.end;482 openModal('schedule-modal');483 }));484 $$('.delete-sched').forEach(btn => btn.addEventListener('click', () => {485 if (!confirm('Delete this schedule?')) return;486 state.schedules = state.schedules.filter(s => s.id !== btn.dataset.id);487 save();488 toast('Schedule deleted', 'success');489 openDetail(roomId); // rerender490 renderRooms();491 }));492 openModal('detail-modal');493 }494 495 // Sections modal496 function renderSections() {497 const ul = $('#section-list');498 ul.innerHTML = state.sections.map((s, idx) => `499 <li class="flex items-center justify-between border border-neutral-200 rounded-md px-3 py-2">500 <div>${s}</div>501 <div class="flex items-center gap-2">502 <button class="text-primary-600 hover:underline text-sm rename-sec" data-index="${idx}">Rename</button>503 <button class="text-danger hover:underline text-sm remove-sec" data-index="${idx}">Remove</button>504 </div>505 </li>506 `).join('');507 $$('.rename-sec').forEach(btn => btn.addEventListener('click', () => {508 const idx = parseInt(btn.dataset.index, 10);509 const curr = state.sections[idx];510 const name = prompt('New section name:', curr);511 if (name && name.trim()) {512 const newName = name.trim();513 // Update schedules referencing the old section514 state.schedules.forEach(sch => {515 if (sch.section === curr) sch.section = newName;516 });517 state.sections[idx] = newName;518 save();519 renderFilters();520 renderSections();521 renderRooms();522 toast('Section renamed', 'success');523 }524 }));525 $$('.remove-sec').forEach(btn => btn.addEventListener('click', () => {526 const idx = parseInt(btn.dataset.index, 10);527 const name = state.sections[idx];528 if (!confirm(`Remove section "${name}"? Schedules tied to it will remain but will show as custom section.`)) return;529 state.sections.splice(idx, 1);530 save();531 renderFilters();532 renderSections();533 renderRooms();534 toast('Section removed', 'success');535 }));536 }537 function openSectionsModal() {538 renderSections();539 openModal('sections-modal');540 }541 // Schedule Graph View542 function generateScheduleGraph() {543 const container = $('#schedule-graph');544 const roomFilter = $('#filter-graph-room').value;545 const sectionFilter = $('#filter-graph-section').value;546 547 // Filter schedules548 let filteredSchedules = state.schedules;549 if (roomFilter) {550 filteredSchedules = filteredSchedules.filter(s => s.roomId === roomFilter);551 }552 if (sectionFilter) {553 filteredSchedules = filteredSchedules.filter(s => s.section === sectionFilter);554 }555 556 if (filteredSchedules.length === 0) {557 container.innerHTML = '<div class="text-center text-neutral-500 py-8">No schedules found for the selected filters.</div>';558 return;559 }560 561 // Create color mapping for sections562 const sectionColors = {};563 const colorPalette = [564 '#3B82F6', '#EF4444', '#10B981', '#F59E0B', '#8B5CF6', 565 '#06B6D4', '#F97316', '#84CC16', '#EC4899', '#6366F1'566 ];567 let colorIndex = 0;568 filteredSchedules.forEach(schedule => {569 if (!sectionColors[schedule.section]) {570 sectionColors[schedule.section] = colorPalette[colorIndex % colorPalette.length];571 colorIndex++;572 }573 });574 575 // Generate legend576 const legend = Object.entries(sectionColors).map(([section, color]) => `577 <div class="legend-item">578 <div class="legend-color" style="background-color: ${color}"></div>579 <span>${section}</span>580 </div>581 `).join('');582 583 // Create chart HTML584 let chartHTML = `585 <div class="legend">586 <div class="font-medium text-sm mr-4">Sections:</div>587 ${legend}588 </div>589 <div class="schedule-chart-container">590 <div class="chart-y-axis">591 <div class="chart-y-label"></div>592 <div class="chart-day" data-day="0">Sunday</div>593 <div class="chart-day" data-day="1">Monday</div>594 <div class="chart-day" data-day="2">Tuesday</div>595 <div class="chart-day" data-day="3">Wednesday</div>596 <div class="chart-day" data-day="4">Thursday</div>597 <div class="chart-day" data-day="5">Friday</div>598 <div class="chart-day" data-day="6">Saturday</div>599 </div>600 <div class="chart-main">601 <div class="chart-x-axis">602 <div class="chart-time-label">7 AM</div>603 <div class="chart-time-label">8 AM</div>604 <div class="chart-time-label">9 AM</div>605 <div class="chart-time-label">10 AM</div>606 <div class="chart-time-label">11 AM</div>607 <div class="chart-time-label">12 PM</div>608 <div class="chart-time-label">1 PM</div>609 <div class="chart-time-label">2 PM</div>610 <div class="chart-time-label">3 PM</div>611 <div class="chart-time-label">4 PM</div>612 <div class="chart-time-label">5 PM</div>613 <div class="chart-time-label">6 PM</div>614 <div class="chart-time-label">7 PM</div>615 <div class="chart-time-label">8 PM</div>616 <div class="chart-time-label">9 PM</div>617 </div>618 <div class="chart-grid">619 `;620 621 // Add schedule bars for each day622 for (let day = 0; day < 7; day++) {623 const daySchedules = filteredSchedules.filter(s => s.days.includes(day));624 625 chartHTML += `<div class="chart-row" data-day="${day}">`;626 627 daySchedules.forEach(schedule => {628 const room = getRoomById(schedule.roomId);629 const startPercent = (toMinutes(schedule.start) - 420) / (1320 - 420) * 100; // 7 AM to 10 PM630 const duration = (toMinutes(schedule.end) - toMinutes(schedule.start)) / (1320 - 420) * 100;631 const isCurrent = isNowActive(schedule);632 633 const bar = `634 <div class="schedule-bar ${isCurrent ? 'current' : ''}" 635 style="left: ${Math.max(0, startPercent)}%; width: ${Math.max(2, duration)}%; background-color: ${sectionColors[schedule.section]}"636 title="${schedule.section} - ${schedule.instructor} (${room ? room.name : 'Unknown Room'}) ${formatTime(schedule.start)} - ${formatTime(schedule.end)}">637 <div class="schedule-bar-content">638 <div class="schedule-bar-title">${schedule.section}</div>639 <div class="schedule-bar-time">${formatTime(schedule.start)} - ${formatTime(schedule.end)}</div>640 <div class="schedule-bar-instructor">${schedule.instructor}</div>641 <div class="schedule-bar-room">${room ? room.name : ''}</div>642 </div>643 </div>644 `;645 chartHTML += bar;646 });647 648 chartHTML += `</div>`;649 }650 651 chartHTML += `652 </div>653 </div>654 </div>655 `;656 657 container.innerHTML = chartHTML;658 659 // Add click events to schedule bars660 $('.schedule-bar').forEach(bar => {661 bar.addEventListener('click', () => {662 const title = bar.getAttribute('title');663 toast(`Schedule: ${title}`, 'info', 5000);664 });665 });666 }667// View toggle functionality668 $('#btn-room-view').addEventListener('click', () => {669 $('#room-view').classList.remove('hidden');670 $('#schedule-view').classList.add('hidden');671 $('#btn-room-view').classList.add('border-primary-600', 'text-primary-600');672 $('#btn-room-view').classList.remove('border-transparent', 'text-neutral-500');673 $('#btn-schedule-view').classList.remove('border-primary-600', 'text-primary-600');674 $('#btn-schedule-view').classList.add('border-transparent', 'text-neutral-500');675 });676 677 $('#btn-schedule-view').addEventListener('click', () => {678 $('#room-view').classList.add('hidden');679 $('#schedule-view').classList.remove('hidden');680 $('#btn-schedule-view').classList.add('border-primary-600', 'text-primary-600');681 $('#btn-schedule-view').classList.remove('border-transparent', 'text-neutral-500');682 $('#btn-room-view').classList.remove('border-primary-600', 'text-primary-600');683 $('#btn-room-view').classList.add('border-transparent', 'text-neutral-500');684 generateScheduleGraph();685 });686 687 // Graph filter events688 $('#filter-graph-room').addEventListener('change', generateScheduleGraph);689 $('#filter-graph-section').addEventListener('change', generateScheduleGraph);690 $('#btn-refresh-graph').addEventListener('click', generateScheduleGraph);691 // Actions in header692 $('#btn-add-room').addEventListener('click', () => {693 if (!roomModal.classList.contains('hidden')) {694 closeModal('room-modal');695 } else {696 openAddRoom();697 }698 });699 $('#btn-add-schedule').addEventListener('click', () => {700 if (!scheduleModal.classList.contains('hidden')) {701 closeModal('schedule-modal');702 } else {703 openScheduleModal();704 }705 });706 $('#btn-manage-sections').addEventListener('click', openSectionsModal);707// Import/Export/Reset708 $('#btn-export').addEventListener('click', () => {709 const dataStr = JSON.stringify(state, null, 2);710 const blob = new Blob([dataStr], { type: 'application/json' });711 const url = URL.createObjectURL(blob);712 const a = document.createElement('a');713 a.href = url;714 a.download = `room-utilization-export-${new Date().toISOString().slice(0,10)}.json`;715 a.click();716 URL.revokeObjectURL(url);717 });718 $('#file-import').addEventListener('change', (e) => {719 const file = e.target.files[0];720 if (!file) return;721 const reader = new FileReader();722 reader.onload = () => {723 try {724 const data = JSON.parse(reader.result);725 if (!data.rooms || !data.schedules || !data.sections) throw new Error('Invalid data shape');726 state = data;727 save();728 renderFilters();729 renderRooms();730 toast('Data imported', 'success');731 } catch (err) {732 console.error(err);733 toast('Failed to import data', 'error');734 }735 };736 reader.readAsText(file);737 e.target.value = '';738 });739 $('#btn-reset').addEventListener('click', () => {740 if (!confirm('Reset all data to defaults? This will erase current data.')) return;741 localStorage.removeItem(STORAGE_KEY);742 state = { rooms: [], sections: [...defaultSections], schedules: [] };743 seed();744 save();745 renderFilters();746 renderRooms();747 toast('Data reset', 'warning');748 });749 750 // Filters751 $('#search-room').addEventListener('input', renderRooms);752 $('#filter-section').addEventListener('change', renderRooms);753 $('#toggle-available').addEventListener('change', renderRooms);754 // Live clock755 function tickClock() {756 const d = new Date();757 $('#live-clock').textContent = `${d.toLocaleString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})`;758 }759 setInterval(() => {760 tickClock();761 }, 1000); // Update clock every second for truly live time762 // Update room occupancy status every second to check for real-time changes763 setInterval(() => {764 renderRooms();765 }, 1000); // Check room status every second766tickClock();767// Toast768 function toast(msg, type = 'success', timeout = 3000) {769 const t = document.createElement('div');770 t.className = `toast ${type}`;771 t.textContent = msg;772 $('#toast').appendChild(t);773 setTimeout(() => t.remove(), timeout);774 }775 776 // Init777 function init() {778 load();779 setupDaysCheckboxes();780 renderFilters();781 renderRooms();782 }783 init();784})();