Skip to content

Instantly share code, notes, and snippets.

@minhqnd
Last active January 17, 2026 02:23
Show Gist options
  • Select an option

  • Save minhqnd/28e0fb5681ab1412163e6f9348ab5a84 to your computer and use it in GitHub Desktop.

Select an option

Save minhqnd/28e0fb5681ab1412163e6f9348ab5a84 to your computer and use it in GitHub Desktop.
Xuất lịch học từ FAP (FPT Academic Portal) sang file ICS để import vào Apple Calendar / Google Calendar.

FAP to Calendar

Xuất lịch học từ FAP (FPT Academic Portal) sang file ICS để import vào Apple Calendar / Google Calendar.

Tính năng

  • Xuất lịch học của tất cả các môn trong kỳ hiện tại
  • Support vị trí 5 cơ sở FPT: Hòa Lạc, Đà Nẵng, Cần Thơ, HCM, Quy Nhơn
  • Tự động thông báo thời gian di chuyển dựa vị trí trên

Sử dụng

  1. Đăng nhập vào FAP: https://fap.fpt.edu.vn
  2. Mở Developer Tools (F12) > Console
  3. Thay đổi CAMPUS ở dòng 7 nếu cần (mặc định là hoa_lac)
  4. Paste toàn bộ script vào console và Enter (nếu lỗi không cho paste thì nhập allow pasting trước vào console rồi mới paste script nhé)
  5. File ICS sẽ tự động tải về
  6. Import file ICS vào Calendar

Cấu hình Campus

const CAMPUS = 'hoa_lac';  // Các lựa chọn: 'hoa_lac', 'da_nang', 'can_tho', 'hcm', 'quy_nhon'

Script xuất lịch

Bấm để mở
// FAP to Calendar - Console Script
// Paste this into browser console on any FAP page after login

// ============================================================
// CONFIG - CHANGE HERE
// ============================================================
const CAMPUS = 'hoa_lac';  // Options: 'hoa_lac', 'da_nang', 'can_tho', 'hcm', 'quy_nhon'
// ============================================================

const CAMPUSES = {
    hoa_lac: {
        name: 'Truong Dai Hoc FPT',
        geo: '21.013148,105.524797',
        mapkit: 'CAESpwMIrk0QmaPutrGyraLOARoSCawDe6ddAzVAEaeB1UeWYVpAIpwBCgdWaWV0bmFtEgJWThoFSGFub2kqClRoYWNoIFRoYXQyClRoYWNoIFRoYXRSFFRoYW5nIExvbmcgQm91bGV2YXJkYhRUaGFuZyBMb25nIEJvdWxldmFyZIoBQUVkdWNhdGlvbiBhbmQgVHJhaW5pbmcgQXJlYSDigJMgSG9hIExhYyBIaWdoLVRlY2ggUGFyayBUaGFjaCBUaGF0KhpUcsaw4budbmcgxJDhuqFpIEjhu41jIEZQVDJkVGhhbmcgTG9uZyBCb3VsZXZhcmQKRWR1Y2F0aW9uIGFuZCBUcmFpbmluZyBBcmVhIOKAkyBIb2EgTGFjIEhpZ2gtVGVjaCBQYXJrClRoYWNoIFRoYXQKSGFub2kKVmlldG5hbTgvUAFaXgooCJmj7raxsq2izgESEgmsA3unXQM1QBGngdVHlmFaQBiuTZADAZgDAaIfMQiZo+62sbKtos4BGiQKGlRyxrDhu51uZyDEkOG6oWkgSOG7jWMgRlBUEAAqAnZpQAA='
    },
    da_nang: {
        name: 'Dai hoc FPT Da Nang',
        geo: '15.967889,108.260694',
        mapkit: 'CAES0QIIrk0Q6rW20Z6b5Yf4ARoSCUZKDjOP7y9AEbKRNTSvEFtAImgKB1ZpZXRuYW0SAlZOGgdEYSBOYW5nKgxOZ3UgSGFuaCBTb24yB0RhIE5hbmdCB0hvYSBIYWmKARZGUFQgVXJiYW4gQXJlYSBEYSBOYW5nigEHSG9hIEhhaYoBDE5ndSBIYW5oIFNvbiocxJDhuqFpIGjhu41jIEZQVCDEkMOgIE7hurVuZzIWRlBUIFVyYmFuIEFyZWEgRGEgTmFuZzIHSG9hIEhhaTIMTmd1IEhhbmggU29uMgdEYSBOYW5nMgdWaWV0bmFtOC9QAVpgCigI6rW20Z6b5Yf4ARISCUZKDjOP7y9AEbKRNTSvEFtAGK5NkAMBmAMBoh8zCOq1ttGem+WH+AEaJgocxJDhuqFpIGjhu41jIEZQVCDEkMOgIE7hurVuZxAAKgJ2aUAA'
    },
    can_tho: {
        name: 'Truong Dai Hoc FPT',
        geo: '10.013006,105.731633',
        mapkit: 'CAES4QIIrk0Qwp3j9q213ro4GhIJVkW4yagGJEARiAp6FNNuWkAifwoHVmlldG5hbRICVk4aB0NhbiBUaG8qCU5pbmggS2lldTIHQ2FuIFRob0IHQW4gQmluaFIUTmd1eWVuIFZhbiBDdSBTdHJlZXRaAzYwMGIZNjAwLCBOZ3V5ZW4gVmFuIEN1IFN0cmVldIoBB0FuIEJpbmiKAQlOaW5oIEtpZXUqGlRyxrDhu51uZyDEkOG6oWkgSOG7jWMgRlBUMhk2MDAsIE5ndXllbiBWYW4gQ3UgU3RyZWV0MgdBbiBCaW5oMglOaW5oIEtpZXUyB0NhbiBUaG8yB1ZpZXRuYW04L1ABWlwKJwjCneP2rbXeujgSEglWRbjJqAYkQBGICnoU025aQBiuTZADAZgDAaIfMAjCneP2rbXeujgaJAoaVHLGsOG7nW5nIMSQ4bqhaSBI4buNYyBGUFQQACoCdmlAAA=='
    },
    hcm: {
        name: 'FPT University HCMC',
        geo: '10.841896,106.808790',
        mapkit: 'CAES3QIIrk0QpOrog/Sy1LjfARoSCY5HX/cMryVAEZz0YzjDs1pAInkKB1ZpZXRuYW0SAlZOGhBIbyBDaGkgTWluaCBDaXR5KgdUaHUgRHVjMhBIbyBDaGkgTWluaCBDaXR5Qg1Mb25nIFRoYW5oIE15UglTdHJlZXQgRDFiCVN0cmVldCBEMYoBDUxvbmcgVGhhbmggTXmKAQdUaHUgRHVjKhxGUFQgVW5pdmVyc2l0eSBIQ01DIFN0dWRlbnRzMglTdHJlZXQgRDEyDUxvbmcgVGhhbmggTXkyB1RodSBEdWMyEEhvIENoaSBNaW5oIENpdHkyB1ZpZXRuYW04L1ABWl4KKAik6uiD9LLUuN8BEhIJjkdf9wyvJUARnPRjOMOzWkAYrk2QAwGYAwGiHzEIpOrog/Sy1LjfARokChxGUFQgVW5pdmVyc2l0eSBIQ01DIFN0dWRlbnRzEAAqAEAA'
    },
    quy_nhon: {
        name: 'FPT University Quy Nhon',
        geo: '13.803885,109.219148',
        mapkit: 'CAESmQIIrk0QvMvU2/XpmIlJGhIJy4XKv5abK0ARlx8ThAZOW0AiQwoHVmlldG5hbRICVk4aCUJpbmggRGluaCoIUXV5IE5ob24yCFF1eSBOaG9uQglOaG9uIEJpbmiKAQlOaG9uIEJpbmgqIUZQVCBVbml2ZXJzaXR5IFF1eSBOaG9uIEFJIENhbXB1czIJTmhvbiBCaW5oMghRdXkgTmhvbjIJQmluaCBEaW5oMgdWaWV0bmFtOC9QAVphCicIvMvU2/XpmIlJEhIJy4XKv5abK0ARlx8ThAZOW0AYrk2QAwGYAwGiHzUIvMvU2/XpmIlJGikKIUZQVCBVbml2ZXJzaXR5IFF1eSBOaG9uIEFJIENhbXB1cxAAKgBAAA=='
    }
};

(async function () {
    const campus = CAMPUSES[CAMPUS] || CAMPUSES.hoa_lac;
    console.log('%cFAP to Calendar @minhqnd', 'font-weight:bold;font-size:14px');

    function parseAttendanceTable(doc, courseCode) {
        const table = doc.querySelector('table.table-bordered');
        if (!table) return [];
        const data = [];
        table.querySelectorAll('tr').forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length >= 7) {
                const sessionNo = cells[0].textContent.trim();
                const dateText = cells[1].querySelector('span')?.textContent.trim() || cells[1].textContent.trim();
                const slotText = cells[2].querySelector('span')?.textContent.trim() || cells[2].textContent.trim();
                const slotMatch = slotText.match(/(\d+)_\((.+)\)/);
                const slotTime = slotMatch ? `(${slotMatch[2]})` : '';
                const room = cells[3].textContent.trim();
                const lecturer = cells[4].textContent.trim();
                const groupName = cells[5].textContent.trim();
                const dateMatch = dateText.match(/(\d{2})\/(\d{2})\/(\d{4})/);
                if (dateMatch) {
                    data.push({
                        subjectCode: courseCode, groupName, sessionNo, lecturer, roomNo: room, slotTime,
                        date: `${parseInt(dateMatch[2])}/${parseInt(dateMatch[1])}/${dateMatch[3]}`
                    });
                }
            }
        });
        return data;
    }

    function extractCourses(doc) {
        const courseDiv = doc.getElementById('ctl00_mainContent_divCourse');
        if (!courseDiv) return [];
        const courses = [];
        const currentBold = courseDiv.querySelector('b');
        if (currentBold) {
            const match = currentBold.textContent.trim().match(/(.+?)\(([A-Z0-9c]+)\)/);
            if (match) courses.push({ code: match[2], href: null, isCurrent: true });
        }
        courseDiv.querySelectorAll('a').forEach(link => {
            const match = link.textContent.trim().match(/(.+?)\(([A-Z0-9c]+)\)/);
            if (match) courses.push({ code: match[2], href: link.getAttribute('href'), isCurrent: false });
        });
        return courses;
    }

    function generateICS(data) {
        const header = `BEGIN:VCALENDAR
METHOD:PUBLISH
VERSION:2.0
X-WR-CALNAME:FPT Schedule
PRODID:-//Apple Inc.//macOS 12.7.4//EN
X-APPLE-CALENDAR-COLOR:#711A76
X-WR-TIMEZONE:Asia/Ho_Chi_Minh
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Asia/Ho_Chi_Minh
BEGIN:STANDARD
TZOFFSETFROM:+0800
DTSTART:19750613T000000
TZNAME:GMT+7
TZOFFSETTO:+0700
RDATE:19750613T000000
END:STANDARD
END:VTIMEZONE`;

        const events = data.map(item => {
            const dp = item.date.match(/(\d+)\/(\d+)\/(\d+)/);
            if (!dp) return '';
            const dateStr = `${dp[3]}${dp[1].padStart(2, '0')}${dp[2].padStart(2, '0')}`;
            const tm = item.slotTime.match(/\((\d+):(\d+)-(\d+):(\d+)\)/);
            if (!tm) return '';
            const uid = crypto.randomUUID().toUpperCase();
            const ts = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
            return `BEGIN:VEVENT
TRANSP:OPAQUE
DTSTART;TZID=Asia/Ho_Chi_Minh:${dateStr}T${tm[1].padStart(2, '0')}${tm[2].padStart(2, '0')}00
DTEND;TZID=Asia/Ho_Chi_Minh:${dateStr}T${tm[3].padStart(2, '0')}${tm[4].padStart(2, '0')}00
UID:${uid}
DTSTAMP:${ts}
LOCATION:${item.roomNo}
DESCRIPTION:${item.lecturer} - ${item.groupName} - Session: ${item.sessionNo}
STATUS:CONFIRMED
SEQUENCE:1
SUMMARY:${item.subjectCode}
LAST-MODIFIED:${ts}
CREATED:${ts}
X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-APPLE-MAPKIT-HANDLE=${campus.mapkit};X-APPLE-RADIUS=141.1745233861194;X-TITLE=${item.roomNo}:geo:${campus.geo}
END:VEVENT`;
        }).filter(e => e);
        return header + '\n' + events.join('\n') + '\nEND:VCALENDAR';
    }

    async function fetchPage(url) {
        const res = await fetch(url, { credentials: 'include' });
        return new DOMParser().parseFromString(await res.text(), 'text/html');
    }

    // Fetch attendance page first (works from any FAP page)
    const attendancePage = await fetchPage('https://fap.fpt.edu.vn/Report/ViewAttendstudent.aspx');

    const courses = extractCourses(attendancePage);
    if (courses.length === 0) {
        console.error('No courses found. Make sure you are logged in.');
        return;
    }
    console.log(`Courses: ${courses.map(c => c.code).join(', ')}`);

    let allData = [];
    let progress = 0;
    const total = courses.length;

    const currentCourse = courses.find(c => c.isCurrent);
    if (currentCourse) {
        const data = parseAttendanceTable(attendancePage, currentCourse.code);
        allData.push(...data);
        progress++;
    }

    for (const course of courses.filter(c => !c.isCurrent && c.href)) {
        try {
            const doc = await fetchPage(`https://fap.fpt.edu.vn/Report/ViewAttendstudent.aspx${course.href}`);
            const data = parseAttendanceTable(doc, course.code);
            allData.push(...data);
            progress++;
            console.log(`Progress: ${progress}/${total}`);
            await new Promise(r => setTimeout(r, 300));
        } catch (e) {
            console.error(`Error: ${course.code}`);
        }
    }

    allData.sort((a, b) => new Date(a.date) - new Date(b.date));

    // Get term name from fetched page
    const termDiv = attendancePage.getElementById('ctl00_mainContent_divTerm');
    const termName = termDiv?.querySelector('b')?.textContent.trim() || 'schedule';
    const filename = termName.replace(/[^a-zA-Z0-9]/g, '_') + '.ics';

    const blob = new Blob([generateICS(allData)], { type: 'text/calendar' });
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    console.log(`Done: ${filename} (${allData.length} events)`);
})();

Import vào Google Calendar

  1. Vào https://calendar.google.com
  2. Settings > Import & Export > Import
  3. Chọn file ICS đã tải

Import vào Apple Calendar

  1. Mở file ICS bằng ứng dụng Calendar
  2. Chọn lịch muốn import
@minhvt22
Copy link

Cảm ơn bác!!!

@letanphat3110-collab
Copy link

Cho mình hỏi nếu trường đổi lecture thì trên calendar sao biết

@minhqnd
Copy link
Author

minhqnd commented Jan 16, 2026

Cho mình hỏi nếu trường đổi lecture thì trên calendar sao biết

thì xuất lại hoặc mỗi slot thì tự sửa thôi :))))), chứ mình chưa gặp bị đổi lecture bao giờ

@Sanj045
Copy link

Sanj045 commented Jan 16, 2026

Hay quá ông ơi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment