feat: import class sessions, attendance, and payments from JSON

Match students by name when national ID or phone is missing, parse Jalali dates, and allow sessions without a professor.
This commit is contained in:
2026-08-16 07:04:16 +03:30
parent 72be01fee3
commit 3348ca5798
7 changed files with 445 additions and 37 deletions
+97
View File
@@ -0,0 +1,97 @@
'use strict';
const toEnglishDigits = (value) =>
String(value ?? '')
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d));
const jalaliToGregorian = (jy, jm, jd) => {
const gy = jy <= 979 ? 621 : 1600;
jy -= jy <= 979 ? 0 : 979;
let days =
365 * jy +
Math.floor(jy / 33) * 8 +
Math.floor(((jy % 33) + 3) / 4) +
78 +
jd +
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
let gyOut = gy + 400 * Math.floor(days / 146097);
days %= 146097;
if (days > 36524) {
gyOut += 100 * Math.floor(--days / 36524);
days %= 36524;
if (days >= 365) days += 1;
}
gyOut += 4 * Math.floor(days / 1461);
days %= 1461;
if (days > 365) {
gyOut += Math.floor((days - 1) / 365);
days = (days - 1) % 365;
}
let gd = days + 1;
const sal_a = [
0,
31,
(gyOut % 4 === 0 && gyOut % 100 !== 0) || gyOut % 400 === 0 ? 29 : 28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31
];
let gm = 0;
for (gm = 1; gm <= 12 && gd > sal_a[gm]; gm += 1) gd -= sal_a[gm];
return `${gyOut}-${String(gm).padStart(2, '0')}-${String(gd).padStart(2, '0')}`;
};
const parseJalaliDate = (raw) => {
if (!raw) return null;
const text = toEnglishDigits(raw).replace(/[./\-]/g, '/').trim();
const match = text.match(/^(\d{3,4})\/(\d{1,2})\/(\d{1,2})$/);
if (!match) return null;
const jy = Number(match[1]);
const jm = Number(match[2]);
const jd = Number(match[3]);
if (!jy || !jm || !jd || jm > 12 || jd > 31 || jy < 1200 || jy > 1599) return null;
try {
return jalaliToGregorian(jy, jm, jd);
} catch {
return null;
}
};
const parseImportDate = (raw) => {
if (!raw) return null;
if (raw instanceof Date) {
return Number.isNaN(raw.getTime()) ? null : raw;
}
const text = String(raw).trim();
if (!text) return null;
const gregorian = text.match(/^(\d{4}-\d{2}-\d{2})/);
if (gregorian) return new Date(`${gregorian[1]}T00:00:00.000Z`);
const jalali = parseJalaliDate(text);
if (jalali) return new Date(`${jalali}T00:00:00.000Z`);
const date = new Date(text);
return Number.isNaN(date.getTime()) ? null : date;
};
const utcDayRange = (value) => {
const date = parseImportDate(value);
if (!date) return null;
const start = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
return { start, end };
};
module.exports = {
toEnglishDigits,
jalaliToGregorian,
parseJalaliDate,
parseImportDate,
utcDayRange
};
+18
View File
@@ -0,0 +1,18 @@
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { jalaliToGregorian, parseJalaliDate, parseImportDate } = require('./jalaliDate');
describe('jalaliDate', () => {
it('converts تکسا attendance dates to Gregorian', () => {
assert.equal(jalaliToGregorian(1405, 4, 23), '2026-07-14');
assert.equal(parseJalaliDate('1405.4.28'), '2026-07-19');
assert.equal(parseJalaliDate('1405/5/20'), '2026-08-11');
});
it('parses ISO and jalali values into UTC midnight dates', () => {
assert.equal(parseImportDate('2026-07-14').toISOString(), '2026-07-14T00:00:00.000Z');
assert.equal(parseImportDate('1405.5.12').toISOString(), '2026-08-03T00:00:00.000Z');
});
});