feat: Background color fetching
All checks were successful
Remote Deploy / deploy (push) Successful in 6s
All checks were successful
Remote Deploy / deploy (push) Successful in 6s
This commit is contained in:
35
package-lock.json
generated
35
package-lock.json
generated
@@ -16,8 +16,10 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"puppeteer": "^24.10.0"
|
||||
"puppeteer": "^24.10.0",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -2634,6 +2636,15 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
||||
@@ -3217,6 +3228,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"puppeteer": "^24.10.0"
|
||||
"puppeteer": "^24.10.0",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,94 @@ import fs from "fs";
|
||||
import parseAbsence from "../utils/parseAbsence.js"
|
||||
import parseTeachers from "../utils/parseTeachers.js"
|
||||
import ExcelJS from "exceljs"
|
||||
import JSZip from "jszip";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
|
||||
/**
|
||||
* Read theme colors from the workbook
|
||||
*/
|
||||
async function getThemeColors(filePath) {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const zip = await JSZip.loadAsync(data);
|
||||
|
||||
// list all files for debug
|
||||
|
||||
const themeFile = zip.file("xl/theme/theme1.xml");
|
||||
if (!themeFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const themeXml = await themeFile.async("text");
|
||||
const theme = await parseStringPromise(themeXml);
|
||||
const scheme = theme["a:theme"]?.["a:themeElements"]?.[0]?.["a:clrScheme"]?.[0];
|
||||
|
||||
if (!scheme) return null;
|
||||
|
||||
function getColor(node) {
|
||||
if (node["a:srgbClr"]) return node["a:srgbClr"][0].$.val;
|
||||
if (node["a:sysClr"]) return node["a:sysClr"][0].$.lastClr;
|
||||
return null;
|
||||
}
|
||||
|
||||
const colors = {
|
||||
0: getColor(scheme["a:dk1"]?.[0]),
|
||||
1: getColor(scheme["a:lt1"]?.[0]),
|
||||
2: getColor(scheme["a:dk2"]?.[0]),
|
||||
3: getColor(scheme["a:lt2"]?.[0]),
|
||||
4: getColor(scheme["a:accent1"]?.[0]),
|
||||
5: getColor(scheme["a:accent2"]?.[0]),
|
||||
6: getColor(scheme["a:accent3"]?.[0]),
|
||||
7: getColor(scheme["a:accent4"]?.[0]),
|
||||
8: getColor(scheme["a:accent5"]?.[0]),
|
||||
9: getColor(scheme["a:accent6"]?.[0]),
|
||||
};
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Excel tint to a base hex color
|
||||
*/
|
||||
function applyTintToHex(hex, tint = 0) {
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
|
||||
const tintChannel = (c) =>
|
||||
tint > 0 ? Math.round(c + (255 - c) * tint) : Math.round(c * (1 + tint));
|
||||
|
||||
const nr = tintChannel(r);
|
||||
const ng = tintChannel(g);
|
||||
const nb = tintChannel(b);
|
||||
|
||||
return [nr, ng, nb]
|
||||
.map((v) => v.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve final hex for a cell fill
|
||||
*/
|
||||
function resolveCellColor(cell, themeColors) {
|
||||
if (!cell.fill?.fgColor) return null;
|
||||
|
||||
const fg = cell.fill.fgColor;
|
||||
|
||||
if (fg.argb) return `#${fg.argb}`;
|
||||
if (fg.theme !== undefined && themeColors) {
|
||||
const base = themeColors[fg.theme];
|
||||
if (!base) return null;
|
||||
return `#${applyTintToHex(base, fg.tint ?? 0)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function parseV3(downloadedFilePath) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(downloadedFilePath);
|
||||
const themeColors = await getThemeColors(downloadedFilePath);
|
||||
|
||||
const teacherMap = await parseTeachers();
|
||||
|
||||
@@ -29,7 +113,7 @@ export default async function parseV3(downloadedFilePath) {
|
||||
const schedule = {};
|
||||
|
||||
for (const { dateKey, sheet } of resolvedDays) {
|
||||
const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap);
|
||||
const { changes, absence, inWork, takesPlace, reservedRooms } = extractDaySchedule(sheet, teacherMap, themeColors);
|
||||
|
||||
schedule[dateKey] = {
|
||||
info: { inWork },
|
||||
@@ -104,9 +188,9 @@ function groupSheetsByDate(items) {
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function extractDaySchedule(sheet, teacherMap) {
|
||||
function extractDaySchedule(sheet, teacherMap, themeColors) {
|
||||
return {
|
||||
changes: extractClassChanges(sheet),
|
||||
changes: extractClassChanges(sheet, themeColors),
|
||||
absence: extractAbsence(sheet, teacherMap),
|
||||
inWork: isPripravaSheet(sheet.name.toLowerCase()),
|
||||
takesPlace: extractTakesPlace(sheet),
|
||||
@@ -128,7 +212,7 @@ function isPripravaSheet(name) {
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function extractClassChanges(sheet) {
|
||||
function extractClassChanges(sheet, themeColors) {
|
||||
const classRegex = /[AEC][0-4][a-c]?\s*\/.*/s;
|
||||
const prefixRegex = /[AEC][0-4][a-c]?/;
|
||||
|
||||
@@ -150,20 +234,20 @@ function extractClassChanges(sheet) {
|
||||
|
||||
classCells.forEach((address, index) => {
|
||||
const row = sheet.getRow(sheet.getCell(address).row);
|
||||
changes[classes[index]] = buildLessonArray(row, address);
|
||||
changes[classes[index]] = buildLessonArray(row, address, themeColors);
|
||||
});
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
function buildLessonArray(row, ignoreAddress) {
|
||||
function buildLessonArray(row, ignoreAddress, themeColors) {
|
||||
const lessons = [];
|
||||
|
||||
row.eachCell((cell) => {
|
||||
if (cell.address === ignoreAddress) return;
|
||||
|
||||
const colIndex = letterToNumber(cell.address.replace(/[0-9]/g, ""));
|
||||
lessons[colIndex] = parseLessonCell(cell);
|
||||
lessons[colIndex] = parseLessonCell(cell, themeColors);
|
||||
});
|
||||
|
||||
const normalized = Array.from(lessons, (x) => (x === undefined ? null : x));
|
||||
@@ -172,15 +256,15 @@ function buildLessonArray(row, ignoreAddress) {
|
||||
return normalized.slice(1, 11);
|
||||
}
|
||||
|
||||
function parseLessonCell(cell) {
|
||||
function parseLessonCell(cell, themeColors) {
|
||||
try {
|
||||
const text = (cell.text || "").trim();
|
||||
const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
|
||||
|
||||
if (!text || cleanupRegex.test(text) || !cell.fill?.fgColor) return null;
|
||||
|
||||
const backgroundColor = cell.fill.fgColor.argb === undefined ? undefined : `#${cell.fill.fgColor.argb}`;
|
||||
const foregroundColor = backgroundColor === undefined ? undefined : (
|
||||
const backgroundColor = resolveCellColor(cell, themeColors);
|
||||
const foregroundColor = !backgroundColor ? undefined : (
|
||||
cell.font?.color?.argb === undefined ? undefined : `#${cell.font.color.argb}`
|
||||
);
|
||||
|
||||
@@ -318,4 +402,4 @@ function formatNowTime() {
|
||||
);
|
||||
}
|
||||
|
||||
//parseV3("db/current.xlsx")
|
||||
parseV3("db/current.xlsx")
|
||||
|
||||
@@ -91,7 +91,7 @@ app.post("/report", async (req, res) => {
|
||||
if (!className || !location || !content) {
|
||||
return res.status(400).json({ error: "Missing required fields." });
|
||||
}
|
||||
if (!["TIMETABLE", "ABSENCES", "OTHER"].includes(location)) {
|
||||
if (!["TIMETABLE", "ABSENCES", "ABSENCE", "TAKES_PLACE", "OTHER"].includes(location)) {
|
||||
return res.status(400).json({ error: "Invalid location value." });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user