1
0

feat: Background color fetching
All checks were successful
Remote Deploy / deploy (push) Successful in 6s

This commit is contained in:
2026-02-10 21:36:53 +01:00
parent cfdd97c935
commit de7ac3a48d
4 changed files with 133 additions and 14 deletions

35
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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")

View File

@@ -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." });
}