v1.4.0: CSV/Excel/JSON/NextUp exports, units-first smart year standing, live export progress bar, AES-GCM at-rest password encryption.
129 lines
4.8 KiB
JavaScript
129 lines
4.8 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory();
|
|
} else {
|
|
root.ISMIStools = Object.assign(root.ISMIStools || {}, factory());
|
|
}
|
|
})(typeof self !== 'undefined' ? self : this, function () {
|
|
'use strict';
|
|
|
|
// RFC-4180 field encoding: quote fields containing commas, quotes, or
|
|
// newlines; double up embedded quotes.
|
|
function csvField(value) {
|
|
let s = String(value == null ? '' : value);
|
|
if (/[",\r\n]/.test(s)) {
|
|
s = '"' + s.replace(/"/g, '""') + '"';
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function csvRow(cells) {
|
|
return cells.map(csvField).join(',') + '\r\n';
|
|
}
|
|
|
|
// Build a CSV representation of the study load.
|
|
// data: { header, courses, totalUnits }
|
|
// header fields: program, studentName, studentId, semester, yearLevel
|
|
function buildStudyLoadCsv(data) {
|
|
const header = data.header || {};
|
|
const lines = [];
|
|
lines.push(csvRow(['ISMIS HELPER - STUDY LOAD']));
|
|
lines.push(csvRow(['Program', header.program || '']));
|
|
lines.push(csvRow(['Student Name', header.fullName || header.studentName || '']));
|
|
lines.push(csvRow(['Student ID', header.studentId || '']));
|
|
lines.push(csvRow(['Semester', header.semester || '']));
|
|
lines.push(csvRow(['Year Level', header.yearLevel || '']));
|
|
lines.push(csvRow(['Total Units', data.totalUnits || '']));
|
|
lines.push(''); // blank separator line
|
|
lines.push(csvRow(['Group', 'Course Code', 'Description', 'Schedule', 'Faculty', 'Units', 'Status']));
|
|
for (const c of (data.courses || [])) {
|
|
lines.push(csvRow([
|
|
c.group, c.courseCode, c.description, c.schedule,
|
|
c.faculty || c.teacher, c.units, c.status
|
|
]));
|
|
}
|
|
lines.push(csvRow(['Total', '', '', '', '', data.totalUnits || '']));
|
|
lines.push(''); // final blank line, per RFC
|
|
// UTF-8 BOM so Excel opens it without mojibake.
|
|
return '\uFEFF' + lines.join('');
|
|
}
|
|
|
|
// Build a Next Up import CSV following the exact column contract in
|
|
// raycast/nextup/src/lib/csv-utils.ts:
|
|
// title, courseCode, days, startTime, endTime, room, meetingLink,
|
|
// color, icon, units, classLink, professorName, professorEmail, groupName
|
|
// One row per schedule block (a course may have multiple meeting segments).
|
|
function buildNextUpCsv(data, parseSchedule) {
|
|
const lines = [];
|
|
lines.push(csvRow(['title', 'courseCode', 'days', 'startTime', 'endTime',
|
|
'room', 'meetingLink', 'color', 'icon', 'units', 'classLink',
|
|
'professorName', 'professorEmail', 'groupName']));
|
|
const resolver = parseSchedule || parseScheduleBlocks;
|
|
const groupName = (data.header && (data.header.semester || '')) || '';
|
|
for (const c of (data.courses || [])) {
|
|
const blocks = resolver(c.schedule);
|
|
if (!blocks.length) {
|
|
lines.push(csvRow([c.description || c.courseCode, c.courseCode, '',
|
|
'', '', '', '', '', '', c.units, '', c.faculty || '', '', groupName]));
|
|
continue;
|
|
}
|
|
for (const b of blocks) {
|
|
lines.push(csvRow([
|
|
c.description || c.courseCode,
|
|
c.courseCode,
|
|
b.days,
|
|
b.startTime,
|
|
b.endTime,
|
|
b.room || '',
|
|
'', '', '',
|
|
c.units,
|
|
'',
|
|
c.faculty || '',
|
|
'',
|
|
groupName
|
|
]));
|
|
}
|
|
}
|
|
return '\uFEFF' + lines.join('');
|
|
}
|
|
|
|
// Split a study-load schedule string like
|
|
// "F 01:30 PM - 04:30 PM LB486TC TC; T Th 03:00 PM - 04:30 PM LB468"
|
|
// into [{ days: 'MO,TU', startTime: '01:30', endTime: '16:30', room }]
|
|
// Day codes map to the 3-letter codes Next Up expects (MO,TU,WE,TH,FR).
|
|
function parseScheduleBlocks(schedule) {
|
|
const s = String(schedule || '').trim();
|
|
if (!s) return [];
|
|
const blocks = [];
|
|
const segRe = /([A-Za-z]{1,3}(?:\s+[A-Za-z]{1,3})*)\s+(\d{1,2}):(\d{2})\s*(AM|PM)\s*-\s*(\d{1,2}):(\d{2})\s*(AM|PM)\s*([A-Za-z0-9]*)/gi;
|
|
const dayMap = {
|
|
M: 'MO', MO: 'MO', MON: 'MO',
|
|
T: 'TU', TU: 'TU', TUE: 'TU',
|
|
W: 'WE', WE: 'WE', WED: 'WE',
|
|
TH: 'TH', THU: 'TH', THUR: 'TH', THURS: 'TH',
|
|
F: 'FR', FR: 'FR', FRI: 'FR',
|
|
SA: 'SA', SAT: 'SA',
|
|
S: 'SU', SU: 'SU', SUN: 'SU', SUNDAY: 'SU'
|
|
};
|
|
let m;
|
|
while ((m = segRe.exec(s)) !== null) {
|
|
const dayToCode = (d) => dayMap[d.toUpperCase().replace(/\./g, '')] || '';
|
|
const days = m[1].split(/\s+/).map(dayToCode).filter(Boolean);
|
|
const to24 = (h, ampm, minute) => {
|
|
let hour = parseInt(h, 10) % 12;
|
|
if (/PM/i.test(ampm)) hour += 12;
|
|
return String(hour).padStart(2, '0') + ':' + minute;
|
|
};
|
|
blocks.push({
|
|
days: days.join(','),
|
|
startTime: to24(m[2], m[4], m[3]),
|
|
endTime: to24(m[5], m[7], m[6]),
|
|
room: m[8] || ''
|
|
});
|
|
}
|
|
return blocks;
|
|
}
|
|
|
|
return { buildStudyLoadCsv, buildNextUpCsv, parseScheduleBlocks, csvField };
|
|
});
|