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",
|
"dotenv": "^17.2.3",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"puppeteer": "^24.10.0"
|
"puppeteer": "^24.10.0",
|
||||||
|
"xml2js": "^0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -2634,6 +2636,15 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/saxes": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
"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": {
|
"node_modules/xmlchars": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"node-cron": "^4.2.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 parseAbsence from "../utils/parseAbsence.js"
|
||||||
import parseTeachers from "../utils/parseTeachers.js"
|
import parseTeachers from "../utils/parseTeachers.js"
|
||||||
import ExcelJS from "exceljs"
|
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) {
|
export default async function parseV3(downloadedFilePath) {
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
await workbook.xlsx.readFile(downloadedFilePath);
|
await workbook.xlsx.readFile(downloadedFilePath);
|
||||||
|
const themeColors = await getThemeColors(downloadedFilePath);
|
||||||
|
|
||||||
const teacherMap = await parseTeachers();
|
const teacherMap = await parseTeachers();
|
||||||
|
|
||||||
@@ -29,7 +113,7 @@ export default async function parseV3(downloadedFilePath) {
|
|||||||
const schedule = {};
|
const schedule = {};
|
||||||
|
|
||||||
for (const { dateKey, sheet } of resolvedDays) {
|
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] = {
|
schedule[dateKey] = {
|
||||||
info: { inWork },
|
info: { inWork },
|
||||||
@@ -104,9 +188,9 @@ function groupSheetsByDate(items) {
|
|||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
|
|
||||||
function extractDaySchedule(sheet, teacherMap) {
|
function extractDaySchedule(sheet, teacherMap, themeColors) {
|
||||||
return {
|
return {
|
||||||
changes: extractClassChanges(sheet),
|
changes: extractClassChanges(sheet, themeColors),
|
||||||
absence: extractAbsence(sheet, teacherMap),
|
absence: extractAbsence(sheet, teacherMap),
|
||||||
inWork: isPripravaSheet(sheet.name.toLowerCase()),
|
inWork: isPripravaSheet(sheet.name.toLowerCase()),
|
||||||
takesPlace: extractTakesPlace(sheet),
|
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 classRegex = /[AEC][0-4][a-c]?\s*\/.*/s;
|
||||||
const prefixRegex = /[AEC][0-4][a-c]?/;
|
const prefixRegex = /[AEC][0-4][a-c]?/;
|
||||||
|
|
||||||
@@ -150,20 +234,20 @@ function extractClassChanges(sheet) {
|
|||||||
|
|
||||||
classCells.forEach((address, index) => {
|
classCells.forEach((address, index) => {
|
||||||
const row = sheet.getRow(sheet.getCell(address).row);
|
const row = sheet.getRow(sheet.getCell(address).row);
|
||||||
changes[classes[index]] = buildLessonArray(row, address);
|
changes[classes[index]] = buildLessonArray(row, address, themeColors);
|
||||||
});
|
});
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLessonArray(row, ignoreAddress) {
|
function buildLessonArray(row, ignoreAddress, themeColors) {
|
||||||
const lessons = [];
|
const lessons = [];
|
||||||
|
|
||||||
row.eachCell((cell) => {
|
row.eachCell((cell) => {
|
||||||
if (cell.address === ignoreAddress) return;
|
if (cell.address === ignoreAddress) return;
|
||||||
|
|
||||||
const colIndex = letterToNumber(cell.address.replace(/[0-9]/g, ""));
|
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));
|
const normalized = Array.from(lessons, (x) => (x === undefined ? null : x));
|
||||||
@@ -172,15 +256,15 @@ function buildLessonArray(row, ignoreAddress) {
|
|||||||
return normalized.slice(1, 11);
|
return normalized.slice(1, 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLessonCell(cell) {
|
function parseLessonCell(cell, themeColors) {
|
||||||
try {
|
try {
|
||||||
const text = (cell.text || "").trim();
|
const text = (cell.text || "").trim();
|
||||||
const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
|
const cleanupRegex = /^úklid\s+(?:\d+\s+)?[A-Za-z]{2}$/;
|
||||||
|
|
||||||
if (!text || cleanupRegex.test(text) || !cell.fill?.fgColor) return null;
|
if (!text || cleanupRegex.test(text) || !cell.fill?.fgColor) return null;
|
||||||
|
|
||||||
const backgroundColor = cell.fill.fgColor.argb === undefined ? undefined : `#${cell.fill.fgColor.argb}`;
|
const backgroundColor = resolveCellColor(cell, themeColors);
|
||||||
const foregroundColor = backgroundColor === undefined ? undefined : (
|
const foregroundColor = !backgroundColor ? undefined : (
|
||||||
cell.font?.color?.argb === undefined ? undefined : `#${cell.font.color.argb}`
|
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) {
|
if (!className || !location || !content) {
|
||||||
return res.status(400).json({ error: "Missing required fields." });
|
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." });
|
return res.status(400).json({ error: "Invalid location value." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user