CoolFace
Apppublic

Saaas007/room-utilization-system

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
script.js784 linesDownload Raw Back to root
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})();