2023-06-10 22:34:28 +02:00
|
|
|
const alasql = require("alasql");
|
2023-06-23 23:00:50 +02:00
|
|
|
const { stores, BUDGET_BRANDS } = require("../model/stores");
|
2023-06-10 22:34:28 +02:00
|
|
|
|
|
|
|
const UNITS = {
|
|
|
|
"stk.": { unit: "stk", factor: 1 },
|
|
|
|
stück: { unit: "stk", factor: 1 },
|
|
|
|
blatt: { unit: "stk", factor: 1 },
|
|
|
|
paar: { unit: "stk", factor: 1 },
|
|
|
|
stk: { unit: "stk", factor: 1 },
|
|
|
|
st: { unit: "stk", factor: 1 },
|
|
|
|
teebeutel: { unit: "stk", factor: 1 },
|
|
|
|
tücher: { unit: "stk", factor: 1 },
|
|
|
|
rollen: { unit: "stk", factor: 1 },
|
|
|
|
tabs: { unit: "stk", factor: 1 },
|
|
|
|
mm: { unit: "cm", factor: 0.1 },
|
|
|
|
cm: { unit: "cm", factor: 1 },
|
|
|
|
zentimeter: { unit: "cm", factor: 1 },
|
|
|
|
m: { unit: "cm", factor: 100 },
|
|
|
|
meter: { unit: "cm", factor: 100 },
|
|
|
|
g: { unit: "g", factor: 1 },
|
|
|
|
gramm: { unit: "g", factor: 1 },
|
|
|
|
dag: { unit: "g", factor: 10 },
|
|
|
|
kg: { unit: "g", factor: 1000 },
|
|
|
|
kilogramm: { unit: "g", factor: 1000 },
|
|
|
|
ml: { unit: "ml", factor: 1 },
|
|
|
|
milliliter: { unit: "ml", factor: 1 },
|
|
|
|
dl: { unit: "ml", factor: 10 },
|
|
|
|
cl: { unit: "ml", factor: 100 },
|
|
|
|
l: { unit: "ml", factor: 1000 },
|
|
|
|
liter: { unit: "ml", factor: 1000 },
|
|
|
|
wg: { unit: "wg", factor: 1 },
|
|
|
|
};
|
|
|
|
|
2023-06-08 13:48:08 +02:00
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
function setupLiveEdit() {
|
|
|
|
if (window.location.host.indexOf("localhost") < 0 && window.location.host.indexOf("127.0.0.1") < 0) return;
|
|
|
|
var script = document.createElement("script");
|
|
|
|
script.type = "text/javascript";
|
|
|
|
script.onload = () => {
|
|
|
|
let lastChangeTimestamp = null;
|
|
|
|
let socket = io({ transports: ["websocket"] });
|
|
|
|
socket.on("message", (timestamp) => {
|
|
|
|
if (lastChangeTimestamp != timestamp) {
|
|
|
|
setTimeout(() => location.reload(), 100);
|
|
|
|
lastChangeTimestamp = timestamp;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
script.src = "js/socket.io.js";
|
|
|
|
document.body.appendChild(script);
|
|
|
|
}
|
|
|
|
setupLiveEdit();
|
|
|
|
}
|
|
|
|
|
2023-06-10 23:43:58 +02:00
|
|
|
exports.isMobile = () => {
|
|
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
|
|
};
|
|
|
|
|
2023-06-10 13:08:08 +02:00
|
|
|
exports.today = () => {
|
|
|
|
const currentDate = new Date();
|
|
|
|
const year = currentDate.getFullYear();
|
|
|
|
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
|
|
|
|
const day = String(currentDate.getDate()).padStart(2, "0");
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
};
|
|
|
|
|
2023-06-08 13:48:08 +02:00
|
|
|
exports.fetchJSON = async (url) => {
|
|
|
|
const response = await fetch(url);
|
|
|
|
return await response.json();
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.downloadJSON = (filename, content) => {
|
2023-06-23 23:00:50 +02:00
|
|
|
exports.downloadFile(filename, JSON.stringify(content, null, 2));
|
|
|
|
};
|
|
|
|
|
|
|
|
exports.downloadFile = (filename, content) => {
|
|
|
|
const blob = new Blob([content], { type: "text/plain" });
|
2023-06-08 13:48:08 +02:00
|
|
|
const element = document.createElement("a");
|
|
|
|
element.href = URL.createObjectURL(blob);
|
|
|
|
element.download = filename;
|
|
|
|
element.style.display = "none";
|
|
|
|
document.body.appendChild(element);
|
|
|
|
element.click();
|
|
|
|
document.body.removeChild(element);
|
|
|
|
URL.revokeObjectURL(element.href);
|
|
|
|
};
|
|
|
|
|
2023-06-09 00:37:29 +02:00
|
|
|
exports.dom = (element, innerHTML) => {
|
|
|
|
const el = document.createElement(element);
|
|
|
|
el.innerHTML = innerHTML;
|
|
|
|
return el;
|
|
|
|
};
|
|
|
|
|
2023-06-11 23:49:18 +02:00
|
|
|
exports.getQueryParameter = (name) => {
|
|
|
|
const url = new URL(window.location.href);
|
|
|
|
const params = url.searchParams.getAll(name);
|
|
|
|
return params.length > 1 ? params : params?.[0];
|
|
|
|
};
|
|
|
|
|
2023-06-10 13:08:08 +02:00
|
|
|
exports.getBooleanAttribute = (element, name) => {
|
|
|
|
return element.hasAttribute(name) && (element.getAttribute(name).length == 0 || element.getAttribute(name) === "true");
|
|
|
|
};
|
2023-06-10 22:34:28 +02:00
|
|
|
|
|
|
|
exports.parseNumber = (value, defaultValue) => {
|
|
|
|
try {
|
|
|
|
return Number.parseFloat(value);
|
|
|
|
} catch (e) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-06-17 14:40:09 +02:00
|
|
|
exports.queryItemsAlasql = (query, items) => {
|
2023-06-14 01:52:35 +02:00
|
|
|
alasql.fn.hasPriceChange = (priceHistory, date, endDate) => {
|
|
|
|
if (!endDate) return priceHistory.some((price) => price.date == date);
|
|
|
|
else return priceHistory.some((price) => price.date >= date && price.date <= endDate);
|
|
|
|
};
|
|
|
|
|
2023-06-14 17:07:02 +02:00
|
|
|
alasql.fn.hasPriceChangeLike = (priceHistory, date) => {
|
|
|
|
return priceHistory.some((price) => price.date.indexOf(date) >= 0);
|
|
|
|
};
|
|
|
|
|
2023-06-17 14:40:09 +02:00
|
|
|
query = query.substring(1);
|
2023-06-21 21:29:57 +02:00
|
|
|
return alasql("select * from ? where " + query, [items]);
|
2023-06-17 14:40:09 +02:00
|
|
|
};
|
2023-06-10 22:34:28 +02:00
|
|
|
|
2023-06-17 14:40:09 +02:00
|
|
|
exports.queryItems = (query, items, exactWord) => {
|
2023-06-22 22:30:54 +02:00
|
|
|
query = query.trim().replace(",", ".").toLowerCase();
|
|
|
|
if (query.length < 3) return { items: [], queryTokens: [] };
|
2023-07-05 20:53:11 +02:00
|
|
|
const regex = /([\p{L}&-\.][\p{L}\p{N}&-\.]*)|(>=|<=|=|>|<)|(\d+(\.\d+)?)/gu;
|
2023-06-22 22:30:54 +02:00
|
|
|
let tokens = query.match(regex);
|
2023-06-10 22:34:28 +02:00
|
|
|
|
|
|
|
// Find quantity/unit query
|
|
|
|
let newTokens = [];
|
|
|
|
let unitQueries = [];
|
2023-06-15 21:23:52 +02:00
|
|
|
const operators = ["<", "<=", ">", ">=", "="];
|
2023-06-10 22:34:28 +02:00
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
|
|
const token = tokens[i];
|
|
|
|
let unit = UNITS[token];
|
|
|
|
if (unit && i > 0 && /^\d+(\.\d+)?$/.test(tokens[i - 1])) {
|
|
|
|
newTokens.pop();
|
|
|
|
let operator = "=";
|
|
|
|
if (i > 1 && operators.includes(tokens[i - 2])) {
|
|
|
|
newTokens.pop();
|
|
|
|
operator = tokens[i - 2];
|
|
|
|
}
|
|
|
|
|
|
|
|
unitQueries.push({
|
|
|
|
operator,
|
|
|
|
quantity: Number.parseFloat(tokens[i - 1]) * unit.factor,
|
|
|
|
unit: unit.unit,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
newTokens.push(token);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tokens = newTokens;
|
|
|
|
|
|
|
|
let hits = [];
|
|
|
|
for (const item of items) {
|
|
|
|
let allFound = true;
|
|
|
|
for (let token of tokens) {
|
|
|
|
if (token.length === 0) continue;
|
|
|
|
let not = false;
|
|
|
|
if (token.startsWith("-") && token.length > 1) {
|
|
|
|
not = true;
|
|
|
|
token = token.substring(1);
|
|
|
|
}
|
|
|
|
const index = item.search.indexOf(token);
|
|
|
|
if ((!not && index < 0) || (not && index >= 0)) {
|
|
|
|
allFound = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (exactWord) {
|
|
|
|
if (index > 0 && item.search.charAt(index - 1) != " " && item.search.charAt(index - 1) != "-") {
|
|
|
|
allFound = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (index + token.length < item.search.length && item.search.charAt(index + token.length) != " ") {
|
|
|
|
allFound = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allFound) {
|
|
|
|
let allUnitsMatched = true;
|
|
|
|
for (const query of unitQueries) {
|
|
|
|
if (query.unit != item.unit) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.operator == "=" && !(item.quantity == query.quantity)) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.operator == "<" && !(item.quantity < query.quantity)) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.operator == "<=" && !(item.quantity <= query.quantity)) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.operator == ">" && !(item.quantity > query.quantity)) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.operator == ">=" && !(item.quantity >= query.quantity)) {
|
|
|
|
allUnitsMatched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allUnitsMatched) hits.push(item);
|
|
|
|
}
|
|
|
|
}
|
2023-06-22 22:17:12 +02:00
|
|
|
return { items: hits, queryTokens: tokens.filter((token) => !token.startsWith("-")) };
|
2023-06-10 22:34:28 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.onVisibleOnce = (target, callback) => {
|
|
|
|
let isTargetVisible = false;
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
|
|
entries.forEach((entry) => {
|
|
|
|
if (entry.target === target && entry.isIntersecting) {
|
|
|
|
if (!isTargetVisible) {
|
|
|
|
isTargetVisible = true;
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
observer.unobserve(target);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
observer.observe(target);
|
|
|
|
};
|
2023-06-11 23:49:18 +02:00
|
|
|
|
2023-06-12 12:26:08 +02:00
|
|
|
exports.log = (message, trace = false) => {
|
2023-06-11 23:49:18 +02:00
|
|
|
const now = new Date();
|
|
|
|
const hours = String(now.getHours()).padStart(2, "0");
|
|
|
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
|
|
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
|
|
|
|
|
|
console.log(`${hours}:${minutes}:${seconds}: ${message}`);
|
2023-06-12 12:26:08 +02:00
|
|
|
if (trace) console.trace("trace");
|
2023-06-11 23:49:18 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
exports.deltaTime = (start) => {
|
|
|
|
return (performance.now() - start) / 1000;
|
|
|
|
};
|
2023-06-23 23:00:50 +02:00
|
|
|
|
|
|
|
exports.itemsToCSV = (items) => {
|
|
|
|
let result = "store;id;name;priceDate;price;isBudgetBrand;quantity;unit;isWeighted;isBio;isAvailable;url\n";
|
|
|
|
for (const item of items) {
|
|
|
|
if (item.store == "lidl" || item.store == "penny") continue;
|
|
|
|
let rowFront = "";
|
|
|
|
rowFront += item.store + ";";
|
|
|
|
rowFront += `"${item.id}"` + ";";
|
|
|
|
rowFront += item.name.replace(";", " ") + ";";
|
|
|
|
|
|
|
|
let rowBack = ";";
|
|
|
|
rowBack += BUDGET_BRANDS.some((budgetBrand) => item.name.toLowerCase().indexOf(budgetBrand) >= 0) + ";";
|
|
|
|
rowBack += item.quantity + ";";
|
|
|
|
rowBack += item.unit + ";";
|
|
|
|
rowBack += (item.isWeighted ?? false) + ";";
|
|
|
|
rowBack += (item.bio ?? false) + ";";
|
|
|
|
rowBack += !(item.unavailable ?? false) + ";";
|
|
|
|
rowBack += stores[item.store].getUrl(item) + ";";
|
|
|
|
|
|
|
|
for (const price of item.priceHistory) {
|
|
|
|
result += rowFront + price.date + ";" + price.price + rowBack + "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
2023-06-24 09:54:24 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @description Formats a number to a locale string with maximum of 2 decimal places with separators
|
|
|
|
* @param {*} number
|
|
|
|
* @returns {string} formatted number
|
|
|
|
* @error returns input if formatting fails
|
|
|
|
*/
|
|
|
|
exports.numberToLocale = (number) => {
|
|
|
|
try {
|
|
|
|
return number.toLocaleString("at-DE", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
|
|
} catch (e) {
|
|
|
|
return number;
|
|
|
|
}
|
|
|
|
};
|