1728 lines
54 KiB
JavaScript
1728 lines
54 KiB
JavaScript
try {
|
|
navigator.wakeLock.request("screen");
|
|
} catch {
|
|
console.log("ScreenLock Failed");
|
|
}
|
|
const debounce = (id, callback, wait, args) => {
|
|
// debounce with trailing callback
|
|
// First call runs immediately, then locks for 'wait' ms
|
|
// If called during lock, saves the latest args and runs once after lock
|
|
// If not called during lock, does nothing
|
|
if (!debounce.timers) {
|
|
debounce.timers = {};
|
|
debounce.args = {};
|
|
}
|
|
if (!debounce.timers[id]) {
|
|
// No lock, run immediately
|
|
debounce.args[id] = Array.isArray(args) ? args : [args];
|
|
// Spread syntax requires ...iterable[Symbol.iterator] to be a function
|
|
callback(...debounce.args[id]);
|
|
debounce.timers[id] = setTimeout(() => {
|
|
if (debounce.args[id]) {
|
|
callback(...debounce.args[id]);
|
|
debounce.args[id] = null;
|
|
debounce.timers[id] = setTimeout(() => {
|
|
debounce.timers[id] = null;
|
|
}, wait);
|
|
} else {
|
|
debounce.timers[id] = null;
|
|
}
|
|
}, wait);
|
|
} else {
|
|
// Lock active, save latest args
|
|
debounce.args[id] = Array.isArray(args) ? args : [args];
|
|
}
|
|
return id;
|
|
};
|
|
|
|
const wheelcolors = [
|
|
// Your original custom colors
|
|
"#ff0000",
|
|
"#ff00ff",
|
|
"#00ff00",
|
|
"#0000ff",
|
|
"#00ffff",
|
|
"#000000",
|
|
"#69DDFF",
|
|
"#7FB800",
|
|
"#963484",
|
|
"#FF1D15",
|
|
"#FF8600",
|
|
|
|
// Precomputed 30° hue-step colors (12 steps, 70% saturation, 50% lightness)
|
|
"#bf3f3f", // 0°
|
|
"#bf9f3f", // 30°
|
|
"#bfff3f", // 60°
|
|
"#7fff3f", // 90°
|
|
"#3fff5f", // 120°
|
|
"#3fffbf", // 150°
|
|
"#3fafff", // 180°
|
|
"#3f3fff", // 210°
|
|
"#9f3fff", // 240°
|
|
"#ff3fff", // 270°
|
|
"#ff3f7f", // 300°
|
|
"#ff3f3f", // 330°
|
|
];
|
|
|
|
// String prototype using the precomputed array
|
|
String.prototype.toHex = function () {
|
|
let hash = 0;
|
|
for (let i = 0; i < this.length; i++) {
|
|
hash = (hash * 31 + this.charCodeAt(i)) >>> 0;
|
|
}
|
|
return wheelcolors[hash % wheelcolors.length];
|
|
};
|
|
|
|
function stringToColour(str) {
|
|
return str.toHex();
|
|
}
|
|
|
|
function colorIsDarkAdvanced(bgColor) {
|
|
let color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
|
let r = parseInt(color.substring(0, 2), 16); // hexToR
|
|
let g = parseInt(color.substring(2, 4), 16); // hexToG
|
|
let b = parseInt(color.substring(4, 6), 16); // hexToB
|
|
let uicolors = [r / 255, g / 255, b / 255];
|
|
let c = uicolors.map((col) => {
|
|
if (col <= 0.03928) {
|
|
return col / 12.92;
|
|
}
|
|
return Math.pow((col + 0.055) / 1.055, 2.4);
|
|
});
|
|
let L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
return L <= 0.179 ? "#FFFFFF" : "#000000";
|
|
}
|
|
|
|
function setLayeredImages(comanda, key) {
|
|
// Base paths for each layer type (adjust paths as needed)
|
|
const basePaths = {
|
|
Selección: "static/ico/layered1/",
|
|
Café: "static/ico/layered1/",
|
|
Endulzante: "static/ico/layered1/",
|
|
Cafeina: "static/ico/layered1/",
|
|
Leche: "static/ico/layered1/",
|
|
};
|
|
|
|
// Map for Selección to filenames
|
|
const selectionMap = {
|
|
"ColaCao con leche": "Selección-ColaCao.png",
|
|
Infusión: "Selección-Infusion.png",
|
|
"Café con leche": "Selección-CaféLeche.png",
|
|
"Solo Leche": "Selección-Leche.png",
|
|
"Solo café (sin leche)": "Selección-CaféSolo.png",
|
|
};
|
|
|
|
// Start div with relative positioning for layering
|
|
let html = `<div style="position: relative; width: 200px; height: 200px; background: white; display: inline-block; border: 1px dotted black;">`;
|
|
|
|
// Layer 1: Selección image
|
|
const selection = comanda["Selección"];
|
|
if (selectionMap[selection]) {
|
|
html += `<img id="img1-${key}" src="${
|
|
basePaths.Selección + selectionMap[selection]
|
|
}" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
|
|
// Layer 2: Café
|
|
if (comanda.Café) {
|
|
html += `<img id="img2-${key}" src="${basePaths.Café}Café-${comanda.Café}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
|
|
// Layer 3: Endulzante
|
|
if (comanda.Endulzante) {
|
|
html += `<img id="img3-${key}" src="${basePaths.Endulzante}Azucar-${comanda.Endulzante}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
|
|
// Layer 4: Cafeina
|
|
if (comanda.Cafeina) {
|
|
html += `<img id="img4-${key}" src="${basePaths.Cafeina}Cafeina-${comanda.Cafeina}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
// Layer 5: Leche
|
|
if (comanda.Leche) {
|
|
html += `<img id="img5-${key}" src="${basePaths.Leche}Leche-${comanda.Leche}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
// Layer 6: Temperatura
|
|
if (comanda.Temperatura) {
|
|
html += `<img id="img6-${key}" src="${basePaths.Leche}Temperatura-${comanda.Temperatura}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
// Layer 7: Tamaño
|
|
if (comanda.Tamaño) {
|
|
html += `<img id="img7-${key}" src="${basePaths.Leche}Tamaño-${comanda.Tamaño}.png" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">`;
|
|
}
|
|
|
|
// Close div
|
|
html += "</div>";
|
|
|
|
return html;
|
|
}
|
|
|
|
function addCategory(
|
|
parent,
|
|
name,
|
|
icon,
|
|
options,
|
|
values,
|
|
change_cb = () => {}
|
|
) {
|
|
var details_0 = document.createElement("details"); // children: img_0, summary_0
|
|
//details_0.open = true;
|
|
var img_0 = document.createElement("img");
|
|
img_0.src = "static/ico/checkbox_unchecked.png";
|
|
img_0.style.height = "30px";
|
|
if (values[name] != undefined) {
|
|
//details_0.open = true;
|
|
img_0.src = "static/ico/checkbox.png";
|
|
}
|
|
var summary_0 = document.createElement("summary");
|
|
var span_0 = document.createElement("span");
|
|
span_0.style.float = "right";
|
|
span_0.append(values[name] || "", " ", img_0);
|
|
summary_0.append(name, span_0);
|
|
details_0.append(summary_0, document.createElement("br"));
|
|
details_0.style.textAlign = "center";
|
|
details_0.style.margin = "5px";
|
|
details_0.style.padding = "5px";
|
|
details_0.style.border = "2px solid black";
|
|
details_0.style.borderRadius = "5px";
|
|
details_0.style.backgroundColor = "white";
|
|
details_0.style.cursor = "pointer";
|
|
details_0.style.width = "calc(100% - 25px)";
|
|
details_0.style.display = "inline-block";
|
|
summary_0.style.padding = "10px";
|
|
// background image at the start of summary_0:
|
|
summary_0.style.backgroundImage = "url('" + icon + "')";
|
|
summary_0.style.backgroundSize = "contain";
|
|
summary_0.style.backgroundPosition = "left";
|
|
summary_0.style.backgroundRepeat = "no-repeat";
|
|
summary_0.style.textAlign = "left";
|
|
summary_0.style.paddingLeft = "55px";
|
|
parent.append(details_0);
|
|
|
|
options.forEach((option) => {
|
|
var btn = document.createElement("button");
|
|
var br1 = document.createElement("br");
|
|
//btn.innerText = option.key + ": " + option.value
|
|
btn.append(option.value);
|
|
// for each image in option.img:
|
|
|
|
if (option.img) {
|
|
var br2 = document.createElement("br");
|
|
btn.append(br2);
|
|
option.img.forEach((imgsrc) => {
|
|
var img = document.createElement("img");
|
|
img.src = imgsrc;
|
|
img.style.height = "50px";
|
|
img.style.padding = "5px";
|
|
img.style.backgroundColor = "white";
|
|
btn.append(img, " ");
|
|
});
|
|
}
|
|
btn.className = option.className;
|
|
if (values[option.key] == option.value) {
|
|
btn.classList.add("activeSCButton");
|
|
}
|
|
btn.onclick = (event) => {
|
|
var items = details_0.getElementsByClassName("activeSCButton");
|
|
for (var i = 0; i < items.length; i++) {
|
|
items[i].classList.remove("activeSCButton");
|
|
}
|
|
btn.classList.add("activeSCButton");
|
|
values[option.key] = option.value;
|
|
span_0.innerText = option.value;
|
|
change_cb(values);
|
|
img_0.src = "static/ico/checkbox.png";
|
|
//details_0.open = false; // Disabled due to request
|
|
};
|
|
btn.style.borderRadius = "20px";
|
|
//btn.style.fontSize="17.5px"
|
|
details_0.append(btn);
|
|
});
|
|
}
|
|
|
|
function addCategory_Personas(
|
|
parent,
|
|
options,
|
|
defaultval,
|
|
change_cb = () => {},
|
|
label = "Persona",
|
|
open_default = false,
|
|
default_empty_text = "- Lista Vacia -"
|
|
) {
|
|
var details_0 = document.createElement("details"); // children: img_0, summary_0
|
|
//details_0.open = true;
|
|
var img_0 = document.createElement("img");
|
|
img_0.src = "static/ico/checkbox_unchecked.png";
|
|
img_0.style.height = "30px";
|
|
if (defaultval != "") {
|
|
details_0.open = false;
|
|
img_0.src = "static/ico/checkbox.png";
|
|
}
|
|
var summary_0 = document.createElement("summary");
|
|
var span_0 = document.createElement("span");
|
|
span_0.style.float = "right";
|
|
var p = SC_Personas[defaultval] || {};
|
|
span_0.append(p.Nombre || "", " ", img_0);
|
|
summary_0.append(label, span_0);
|
|
details_0.append(summary_0, document.createElement("br"));
|
|
if (open_default == true) {
|
|
details_0.open = true;
|
|
}
|
|
details_0.style.textAlign = "center";
|
|
details_0.style.margin = "5px";
|
|
details_0.style.padding = "5px";
|
|
details_0.style.border = "2px solid black";
|
|
details_0.style.borderRadius = "5px";
|
|
details_0.style.backgroundColor = "white";
|
|
details_0.style.cursor = "pointer";
|
|
details_0.style.width = "calc(100% - 25px)";
|
|
details_0.style.display = "inline-block";
|
|
summary_0.style.padding = "10px";
|
|
// background image at the start of summary_0:
|
|
summary_0.style.backgroundImage = "url('static/ico/user.png')";
|
|
summary_0.style.backgroundSize = "contain";
|
|
summary_0.style.backgroundPosition = "left";
|
|
summary_0.style.backgroundRepeat = "no-repeat";
|
|
summary_0.style.textAlign = "left";
|
|
summary_0.style.paddingLeft = "55px";
|
|
parent.append(details_0);
|
|
var lastreg = "";
|
|
Object.entries(options)
|
|
.map(([_, data]) => {
|
|
data["_key"] = _;
|
|
return data
|
|
})
|
|
.sort(betterSorter)
|
|
.map((entry) => {
|
|
var key = entry["_key"];
|
|
var value = entry;
|
|
if (lastreg != value.Region.toUpperCase()) {
|
|
lastreg = value.Region.toUpperCase();
|
|
var h3_0 = document.createElement("h2");
|
|
h3_0.style.margin = "0";
|
|
h3_0.style.marginTop = "15px";
|
|
h3_0.innerText = lastreg;
|
|
details_0.append(h3_0);
|
|
}
|
|
var option = value.Nombre;
|
|
var btn = document.createElement("button");
|
|
var br1 = document.createElement("br");
|
|
//btn.innerText = option.key + ": " + option.value
|
|
btn.append(option);
|
|
|
|
var br2 = document.createElement("br");
|
|
btn.append(br2);
|
|
var img = document.createElement("img");
|
|
img.src = value.Foto || "static/ico/user_generic.png";
|
|
// Prefer attachment 'foto' for this persona
|
|
try {
|
|
const personaKey = key;
|
|
if (personaKey) {
|
|
DB.getAttachment('personas', personaKey, 'foto').then((durl) => {
|
|
if (durl) img.src = durl;
|
|
}).catch(() => {});
|
|
}
|
|
} catch (e) {}
|
|
img.style.height = "60px";
|
|
img.style.padding = "5px";
|
|
img.style.backgroundColor = "white";
|
|
btn.append(img, " ");
|
|
|
|
if (defaultval == key) {
|
|
btn.classList.add("activeSCButton");
|
|
}
|
|
btn.onclick = (event) => {
|
|
var items = details_0.getElementsByClassName("activeSCButton");
|
|
for (var i = 0; i < items.length; i++) {
|
|
items[i].classList.remove("activeSCButton");
|
|
}
|
|
btn.classList.add("activeSCButton");
|
|
defaultval = key;
|
|
span_0.innerText = "";
|
|
var img_5 = document.createElement("img");
|
|
img_5.src = value.Foto || "static/ico/user_generic.png";
|
|
// Prefer attachment 'foto' when available
|
|
try {
|
|
const personaKey2 = key;
|
|
if (personaKey2) {
|
|
DB.getAttachment('personas', personaKey2, 'foto').then((durl) => {
|
|
if (durl) img_5.src = durl;
|
|
}).catch(() => {});
|
|
}
|
|
} catch (e) {}
|
|
img_5.style.height = "30px";
|
|
span_0.append(img_5, value.Nombre);
|
|
change_cb(defaultval);
|
|
img_0.src = "static/ico/checkbox.png";
|
|
//details_0.open = false; // Disabled due to request
|
|
};
|
|
btn.style.borderRadius = "20px";
|
|
//btn.style.fontSize="17.5px"
|
|
details_0.append(btn);
|
|
});
|
|
if (Object.entries(options).length == 0) {
|
|
var btn = document.createElement("b");
|
|
btn.append(default_empty_text);
|
|
details_0.append(btn);
|
|
}
|
|
}
|
|
const SC_actions_icons = {
|
|
Tamaño: "static/ico/sizes.png",
|
|
Temperatura: "static/ico/thermometer2.png",
|
|
Leche: "static/ico/milk.png",
|
|
Selección: "static/ico/preferences.png",
|
|
Cafeina: "static/ico/coffee_bean.png",
|
|
Endulzante: "static/ico/lollipop.png",
|
|
Receta: "static/ico/cookies.png",
|
|
};
|
|
const SC_actions = {
|
|
Selección: [
|
|
{
|
|
value: "Solo Leche",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/milk.png"],
|
|
},
|
|
{
|
|
value: "Solo café (sin leche)",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/coffee_bean.png"],
|
|
},
|
|
{
|
|
value: "Café con leche",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/coffee_bean.png", "static/ico/milk.png"],
|
|
},
|
|
{
|
|
value: "ColaCao con leche",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/colacao.jpg", "static/ico/milk.png"],
|
|
},
|
|
{
|
|
value: "Leche con cereales",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/cereales.png", "static/ico/milk.png"],
|
|
},
|
|
{
|
|
value: "Infusión",
|
|
key: "Selección",
|
|
className: "btn4",
|
|
img: ["static/ico/tea_bag.png"],
|
|
},
|
|
],
|
|
Tamaño: [
|
|
{
|
|
value: "Grande",
|
|
key: "Tamaño",
|
|
className: "btn1",
|
|
img: ["static/ico/keyboard_key_g.png"],
|
|
},
|
|
{
|
|
value: "Pequeño",
|
|
key: "Tamaño",
|
|
className: "btn1",
|
|
img: ["static/ico/keyboard_key_p.png"],
|
|
},
|
|
],
|
|
Temperatura: [
|
|
{
|
|
value: "Caliente",
|
|
key: "Temperatura",
|
|
className: "btn2",
|
|
img: [
|
|
"static/ico/thermometer2.png",
|
|
"static/ico/arrow_up_red.png",
|
|
"static/ico/fire.png",
|
|
],
|
|
},
|
|
{
|
|
value: "Templado",
|
|
key: "Temperatura",
|
|
className: "btn2",
|
|
img: ["static/ico/thermometer2.png", "static/ico/arrow_left_green.png"],
|
|
},
|
|
{
|
|
value: "Frio",
|
|
key: "Temperatura",
|
|
className: "btn2",
|
|
img: [
|
|
"static/ico/thermometer2.png",
|
|
"static/ico/arrow_down_blue.png",
|
|
"static/ico/snowflake.png",
|
|
],
|
|
},
|
|
],
|
|
Leche: [
|
|
{
|
|
value: "de Vaca",
|
|
key: "Leche",
|
|
className: "btn3",
|
|
img: ["static/ico/cow.png", "static/ico/add.png"],
|
|
},
|
|
{
|
|
value: "Sin lactosa",
|
|
key: "Leche",
|
|
className: "btn3",
|
|
img: ["static/ico/cow.png", "static/ico/delete.png"],
|
|
},
|
|
{
|
|
value: "Vegetal",
|
|
key: "Leche",
|
|
className: "btn3",
|
|
img: ["static/ico/milk.png", "static/ico/wheat.png"],
|
|
},
|
|
{
|
|
value: "Almendras",
|
|
key: "Leche",
|
|
className: "btn3",
|
|
img: ["static/ico/milk.png", "static/ico/almond.svg"],
|
|
},
|
|
{
|
|
value: "Agua",
|
|
key: "Leche",
|
|
className: "btn3",
|
|
img: ["static/ico/water_tap.png"],
|
|
},
|
|
],
|
|
Cafeina: [
|
|
{
|
|
value: "Con",
|
|
key: "Cafeina",
|
|
className: "btn5",
|
|
img: ["static/ico/coffee_bean.png", "static/ico/add.png"],
|
|
},
|
|
{
|
|
value: "Sin",
|
|
key: "Cafeina",
|
|
className: "btn5",
|
|
img: ["static/ico/coffee_bean.png", "static/ico/delete.png"],
|
|
},
|
|
],
|
|
Endulzante: [
|
|
{
|
|
value: "Az. Blanco",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/azucar-blanco.jpg"],
|
|
},
|
|
{
|
|
value: "Az. Moreno",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/azucar-moreno.png"],
|
|
},
|
|
{
|
|
value: "Sacarina",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/sacarina.jpg"],
|
|
},
|
|
{
|
|
value: "Stevia (Pastillas)",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/stevia.jpg"],
|
|
},
|
|
{
|
|
value: "Stevia (Gotas)",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/stevia-gotas.webp"],
|
|
},
|
|
{
|
|
value: "Sin",
|
|
key: "Endulzante",
|
|
className: "btn6",
|
|
img: ["static/ico/delete.png"],
|
|
},
|
|
],
|
|
Receta: [
|
|
{
|
|
value: "Si",
|
|
key: "Receta",
|
|
className: "btn7",
|
|
img: ["static/ico/add.png"],
|
|
},
|
|
{
|
|
value: "No",
|
|
key: "Receta",
|
|
className: "btn7",
|
|
img: ["static/ico/delete.png"],
|
|
},
|
|
],
|
|
};
|
|
function TS_decrypt(input, secret, callback, table, id) {
|
|
// Accept objects or plaintext strings. Support AES-encrypted entries wrapped as RSA{...}.
|
|
if (typeof input !== "string") {
|
|
try { callback(input, false); } catch (e) { console.error(e); }
|
|
return;
|
|
}
|
|
|
|
// Encrypted format marker: RSA{<ciphertext>} where <ciphertext> is CryptoJS AES output
|
|
if (input.startsWith("RSA{") && input.endsWith("}") && typeof CryptoJS !== 'undefined') {
|
|
try {
|
|
var data = input.slice(4, -1);
|
|
var words = CryptoJS.AES.decrypt(data, secret);
|
|
var decryptedUtf8 = null;
|
|
try {
|
|
decryptedUtf8 = words.toString(CryptoJS.enc.Utf8);
|
|
} catch (utfErr) {
|
|
try {
|
|
decryptedUtf8 = words.toString(CryptoJS.enc.Latin1);
|
|
} catch (latinErr) {
|
|
console.warn('TS_decrypt: failed to decode decrypted bytes', utfErr, latinErr);
|
|
try { callback(input, false); } catch (ee) { }
|
|
return;
|
|
}
|
|
}
|
|
var parsed = null;
|
|
try { parsed = JSON.parse(decryptedUtf8); } catch (pe) { parsed = decryptedUtf8; }
|
|
try { callback(parsed, true); } catch (e) { console.error(e); }
|
|
// Keep encrypted at-rest: if table/id provided, ensure DB stores encrypted payload (input)
|
|
if (table && id && window.DB && DB.put) {
|
|
DB.put(table, id, input).catch(() => {});
|
|
}
|
|
return;
|
|
} catch (e) {
|
|
console.error('TS_decrypt: invalid encrypted payload', e);
|
|
try { callback(input, false); } catch (ee) { }
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Plain JSON stored as text -> parse and return, then re-encrypt in DB for at-rest protection
|
|
try {
|
|
var parsed = JSON.parse(input);
|
|
try { callback(parsed, false); } catch (e) { console.error(e); }
|
|
if (table && id && window.DB && DB.put && typeof SECRET !== 'undefined') {
|
|
TS_encrypt(parsed, SECRET, function (enc) {
|
|
DB.put(table, id, enc).catch(() => {});
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Not JSON, return raw string
|
|
try { callback(input, false); } catch (err) { console.error(err); }
|
|
}
|
|
}
|
|
function TS_encrypt(input, secret, callback, mode = "RSA") {
|
|
// Encrypt given value for at-rest storage using CryptoJS AES.
|
|
// Always return string of form RSA{<ciphertext>} via callback.
|
|
try {
|
|
if (typeof CryptoJS === 'undefined') {
|
|
// CryptoJS not available — return plaintext
|
|
try { callback(input); } catch (e) { console.error(e); }
|
|
return;
|
|
}
|
|
var payload = input;
|
|
if (typeof input !== 'string') {
|
|
try { payload = JSON.stringify(input); } catch (e) { payload = String(input); }
|
|
}
|
|
var encrypted = CryptoJS.AES.encrypt(payload, secret).toString();
|
|
var out = 'RSA{' + encrypted + '}';
|
|
try { callback(out); } catch (e) { console.error(e); }
|
|
} catch (e) {
|
|
console.error('TS_encrypt: encryption failed', e);
|
|
try { callback(input); } catch (err) { console.error(err); }
|
|
}
|
|
}
|
|
// Listado precargado de personas:
|
|
DB.map('personas', (data, key) => {
|
|
function add_row(data, key) {
|
|
if (data != null) {
|
|
data["_key"] = key;
|
|
SC_Personas[key] = data;
|
|
} else {
|
|
delete SC_Personas[key];
|
|
}
|
|
}
|
|
if (typeof data == "string") {
|
|
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
|
add_row(data, key);
|
|
}, 'personas', key);
|
|
} else {
|
|
add_row(data, key);
|
|
}
|
|
});
|
|
|
|
function SC_parse(json) {
|
|
var out = "";
|
|
Object.entries(json).forEach((entry) => {
|
|
out += entry[0] + ": " + entry[1] + "\n";
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function SC_parse_short(json) {
|
|
var valores = "<small style='font-size: 60%;'>Servicio base (10c)</small>\n";
|
|
|
|
Object.entries(json).forEach((entry) => {
|
|
valores +=
|
|
"<small style='font-size: 60%;'>" +
|
|
entry[0] +
|
|
":</small> " +
|
|
entry[1] +
|
|
" ";
|
|
var combo = entry[0] + ";" + entry[1];
|
|
switch (entry[0]) {
|
|
case "Leche":
|
|
// Leche pequeña = 10c
|
|
if (
|
|
json["Tamaño"] == "Pequeño" &&
|
|
["de Vaca", "Sin lactosa", "Vegetal", "Almendras"].includes(
|
|
json["Leche"]
|
|
)
|
|
) {
|
|
valores += "<small>(P = 10c)</small>";
|
|
}
|
|
// Leche grande = 20c
|
|
if (
|
|
json["Tamaño"] == "Grande" &&
|
|
["de Vaca", "Sin lactosa", "Vegetal", "Almendras"].includes(
|
|
json["Leche"]
|
|
)
|
|
) {
|
|
valores += "<small>(G = 20c)</small>";
|
|
}
|
|
break;
|
|
case "Selección":
|
|
// Café = 20c
|
|
if (
|
|
["Café con leche", "Solo café (sin leche)"].includes(
|
|
json["Selección"]
|
|
)
|
|
) {
|
|
valores += "<small>(20c)</small>";
|
|
}
|
|
// ColaCao = 20c
|
|
if (json["Selección"] == "ColaCao con leche") {
|
|
valores += "<small>(20c)</small>";
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
valores += "\n";
|
|
});
|
|
return valores;
|
|
}
|
|
|
|
function SC_priceCalc(json) {
|
|
var precio = 0;
|
|
var valores = "";
|
|
// Servicio base = 10c
|
|
precio += 10;
|
|
valores += "Servicio base = 10c\n";
|
|
// Leche pequeña = 10c
|
|
if (
|
|
json["Tamaño"] == "Pequeño" &&
|
|
["de Vaca", "Sin lactosa", "Vegetal", "Almendras"].includes(json["Leche"])
|
|
) {
|
|
precio += 10;
|
|
valores += "Leche pequeña = 10c\n";
|
|
}
|
|
// Leche grande = 20c
|
|
if (
|
|
json["Tamaño"] == "Grande" &&
|
|
["de Vaca", "Sin lactosa", "Vegetal", "Almendras"].includes(json["Leche"])
|
|
) {
|
|
precio += 20;
|
|
valores += "Leche grande = 20c\n";
|
|
}
|
|
// Café = 20c
|
|
if (["Café con leche", "Solo café (sin leche)"].includes(json["Selección"])) {
|
|
precio += 20;
|
|
valores += "Café = 20c\n";
|
|
}
|
|
// ColaCao = 20c
|
|
if (json["Selección"] == "ColaCao con leche") {
|
|
precio += 20;
|
|
valores += "ColaCao = 20c\n";
|
|
}
|
|
valores += "<hr>Total: " + precio + "c\n";
|
|
return [precio, valores];
|
|
}
|
|
|
|
function TS_IndexElement(
|
|
pageco,
|
|
config,
|
|
ref,
|
|
container,
|
|
rowCallback = undefined,
|
|
canAddCallback = undefined,
|
|
globalSearchBar = true
|
|
) {
|
|
// Every item in config should have:
|
|
// key: string
|
|
// type: string
|
|
// default: string
|
|
// label: string
|
|
var tablebody = safeuuid();
|
|
var tablehead = safeuuid();
|
|
var scrolltable = safeuuid();
|
|
var searchKeyInput = safeuuid();
|
|
var debounce_search = safeuuid();
|
|
var debounce_load = safeuuid();
|
|
|
|
// Create the container with search bar and table
|
|
container.innerHTML = `
|
|
<div id="${scrolltable}">
|
|
<table>
|
|
<thead>
|
|
<tr style="background: transparent;">
|
|
<th colspan="100%" style="padding: 0; background: transparent;">
|
|
<input type="text" id="${searchKeyInput}" placeholder="🔍 Buscar..." style="width: calc(100% - 18px); padding: 8px; border: 1px solid #ccc; border-radius: 4px; background-color: rebeccapurple; color: white;">
|
|
</th>
|
|
</tr>
|
|
<tr id="${tablehead}"></tr>
|
|
</thead>
|
|
<tbody id="${tablebody}">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
tableScroll("#" + scrolltable); // id="scrolltable"
|
|
var tablehead_EL = document.getElementById(tablehead);
|
|
var tablebody_EL = document.getElementById(tablebody);
|
|
var rows = {};
|
|
config.forEach((key) => {
|
|
tablehead_EL.innerHTML += `<th>${key.label || ""}</th>`;
|
|
});
|
|
// Add search functionality
|
|
const searchKeyEl = document.getElementById(searchKeyInput);
|
|
searchKeyEl.addEventListener("input", () =>
|
|
debounce(debounce_search, render, 300, [rows])
|
|
);
|
|
|
|
function searchInData(data, searchValue, config) {
|
|
if (!searchValue) return true;
|
|
|
|
// Search in ID
|
|
if (data._key.toLowerCase().includes(searchValue)) return true;
|
|
|
|
// Search in configured fields
|
|
for (var field of config) {
|
|
const value = data[field.key] || field.default || "";
|
|
|
|
// Handle different field types
|
|
switch (field.type) {
|
|
case "comanda":
|
|
try {
|
|
const comandaData = JSON.parse(data.Comanda);
|
|
// Search in all comanda fields
|
|
if (
|
|
Object.values(comandaData).some((v) =>
|
|
String(v).toLowerCase().includes(searchValue)
|
|
)
|
|
)
|
|
return true;
|
|
} catch (e) {
|
|
// If JSON parse fails, search in raw string
|
|
if (data.Comanda.toLowerCase().includes(searchValue)) return true;
|
|
}
|
|
break;
|
|
case "persona":
|
|
case "persona-nombre":
|
|
var persona = SC_Personas[value] || { Nombre: "", Region: "" };
|
|
if (field.self == true) {
|
|
persona = data || { Nombre: "", Region: "" };
|
|
}
|
|
if (persona) {
|
|
// Search in persona fields
|
|
if (persona.Nombre.toLowerCase().includes(searchValue)) return true;
|
|
if (persona.Region.toLowerCase().includes(searchValue)) return true;
|
|
}
|
|
break;
|
|
case "fecha":
|
|
case "fecha-iso":
|
|
// Format date as DD/MM/YYYY for searching
|
|
if (value) {
|
|
const fechaArray = value.split("-");
|
|
const formattedDate = `${fechaArray[2]}/${fechaArray[1]}/${fechaArray[0]}`;
|
|
if (formattedDate.includes(searchValue)) return true;
|
|
}
|
|
break;
|
|
default:
|
|
// For raw and other types, search in the direct value
|
|
if (String(value).toLowerCase().includes(searchValue)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// --- Optimized render function ---
|
|
var lastSearchValue = "";
|
|
var lastFilteredSorted = [];
|
|
|
|
|
|
function getFilteredSortedRows(searchValue) {
|
|
// Only use cache if searchValue is not empty and cache is valid
|
|
if (
|
|
searchValue &&
|
|
searchValue === lastSearchValue &&
|
|
lastFilteredSorted.length > 0
|
|
) {
|
|
return lastFilteredSorted;
|
|
}
|
|
const filtered = Object.entries(rows)
|
|
.filter(([_, data]) => searchInData(data, searchValue, config))
|
|
.map(([_, data]) => data)
|
|
.sort(betterSorter);
|
|
lastSearchValue = searchValue;
|
|
lastFilteredSorted = filtered;
|
|
return filtered;
|
|
}
|
|
|
|
function render(rows) {
|
|
const searchValue = searchKeyEl.value.toLowerCase().trim();
|
|
// Use document fragment for batch DOM update
|
|
const fragment = document.createDocumentFragment();
|
|
const filteredSorted = getFilteredSortedRows(searchValue);
|
|
for (let i = 0; i < filteredSorted.length; i++) {
|
|
const data = filteredSorted[i];
|
|
if (canAddCallback != undefined && canAddCallback(data) === true) {
|
|
continue;
|
|
}
|
|
const new_tr = document.createElement("tr");
|
|
if (rowCallback != undefined) {
|
|
rowCallback(data, new_tr);
|
|
}
|
|
config.forEach((key) => {
|
|
switch (key.type) {
|
|
case "_encrypted": {
|
|
const tdEncrypted = document.createElement("td");
|
|
tdEncrypted.innerText = data["_encrypted__"]
|
|
? "🔒"
|
|
: "";
|
|
new_tr.appendChild(tdEncrypted);
|
|
break;
|
|
}
|
|
case "raw":
|
|
case "text": {
|
|
const tdRaw = document.createElement("td");
|
|
const rawContent = (
|
|
String(data[key.key]) ||
|
|
key.default ||
|
|
""
|
|
).replace(/\n/g, "<br>");
|
|
tdRaw.innerHTML = rawContent;
|
|
new_tr.appendChild(tdRaw);
|
|
break;
|
|
}
|
|
case "moneda": {
|
|
const tdMoneda = document.createElement("td");
|
|
const valor = parseFloat(data[key.key]);
|
|
if (!isNaN(valor)) {
|
|
tdMoneda.innerText = valor.toFixed(2) + " €";
|
|
} else {
|
|
tdMoneda.innerText = key.default || "";
|
|
}
|
|
new_tr.appendChild(tdMoneda);
|
|
break;
|
|
}
|
|
case "fecha":
|
|
case "fecha-iso": {
|
|
const tdFechaISO = document.createElement("td");
|
|
if (data[key.key]) {
|
|
const fechaArray = data[key.key].split("-");
|
|
tdFechaISO.innerText =
|
|
fechaArray[2] + "/" + fechaArray[1] + "/" + fechaArray[0];
|
|
}
|
|
new_tr.appendChild(tdFechaISO);
|
|
break;
|
|
}
|
|
case "fecha-diff": {
|
|
const tdFechaISO = document.createElement("td");
|
|
if (data[key.key]) {
|
|
const fecha = new Date(data[key.key]);
|
|
const now = new Date();
|
|
const diffTime = Math.abs(now - fecha);
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
const diffMonths = Math.floor(diffDays / 30);
|
|
const diffYears = Math.floor(diffDays / 365);
|
|
let diffString = "";
|
|
if (diffYears > 0) {
|
|
diffString += diffYears + " año" + (diffYears > 1 ? "s " : " ");
|
|
}
|
|
if (diffMonths % 12 > 0) {
|
|
diffString +=
|
|
(diffMonths % 12) +
|
|
" mes" +
|
|
(diffMonths % 12 > 1 ? "es " : " ");
|
|
}
|
|
|
|
// if more than 3 months, show rgb(255, 192, 192) as background
|
|
if (diffMonths >= 3) {
|
|
tdFechaISO.style.backgroundColor = "rgb(255, 192, 192)";
|
|
} else if (diffMonths >= 1) {
|
|
tdFechaISO.style.backgroundColor = "rgb(252, 252, 176)";
|
|
}
|
|
tdFechaISO.innerText = diffString.trim();
|
|
}
|
|
new_tr.appendChild(tdFechaISO);
|
|
break;
|
|
}
|
|
case "template": {
|
|
const tdCustomTemplate = document.createElement("td");
|
|
new_tr.appendChild(tdCustomTemplate);
|
|
key.template(data, tdCustomTemplate);
|
|
break;
|
|
}
|
|
case "comanda": {
|
|
const tdComanda = document.createElement("td");
|
|
tdComanda.style.verticalAlign = "top";
|
|
const parsedComanda = JSON.parse(data.Comanda);
|
|
const precio = SC_priceCalc(parsedComanda)[0];
|
|
const tempDiv = document.createElement("div");
|
|
tempDiv.innerHTML = setLayeredImages(parsedComanda, data._key);
|
|
tdComanda.appendChild(tempDiv.firstChild);
|
|
const pre = document.createElement("pre");
|
|
pre.style.fontSize = "15px";
|
|
pre.style.display = "inline-block";
|
|
pre.style.margin = "0";
|
|
pre.style.verticalAlign = "top";
|
|
pre.style.padding = "5px";
|
|
pre.style.background = "rgba(255, 255, 0, 0.5)";
|
|
pre.style.border = "1px solid rgba(0, 0, 0, 0.2)";
|
|
pre.style.borderRadius = "5px";
|
|
pre.style.boxShadow = "2px 2px 5px rgba(0, 0, 0, 0.1)";
|
|
pre.style.height = "100%";
|
|
const spanPrecio = document.createElement("span");
|
|
spanPrecio.style.fontSize = "20px";
|
|
spanPrecio.innerHTML = `Total: ${precio}c`;
|
|
pre.innerHTML = "<b>Ticket de compra</b> ";
|
|
pre.appendChild(document.createTextNode("\n"));
|
|
pre.innerHTML +=
|
|
SC_parse_short(parsedComanda) + "<hr>" + data.Notas + "<hr>";
|
|
pre.appendChild(spanPrecio);
|
|
tdComanda.appendChild(pre);
|
|
new_tr.appendChild(tdComanda);
|
|
break;
|
|
}
|
|
case "comanda-status": {
|
|
var sc_nobtn = "";
|
|
if (urlParams.get("sc_nobtn") == "yes") {
|
|
sc_nobtn = "pointer-events: none; opacity: 0.5";
|
|
}
|
|
const td = document.createElement("td");
|
|
td.style.fontSize = "17px";
|
|
if (sc_nobtn) {
|
|
td.style.pointerEvents = "none";
|
|
td.style.opacity = "0.5";
|
|
}
|
|
const createButton = (text, state) => {
|
|
const button = document.createElement("button");
|
|
button.textContent = text;
|
|
if (data.Estado === state) {
|
|
button.className = "rojo";
|
|
}
|
|
button.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
data.Estado = state;
|
|
if (typeof ref === 'string') {
|
|
DB.put(ref, data._key, data).then(() => {
|
|
toastr.success("Guardado!");
|
|
}).catch((e) => { console.warn('DB.put error', e); });
|
|
} else {
|
|
try {
|
|
// legacy
|
|
ref.get(data._key).put(data);
|
|
toastr.success("Guardado!");
|
|
} catch (e) {
|
|
console.warn('Could not save item', e);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
return button;
|
|
};
|
|
const buttons = [
|
|
createButton("Pedido", "Pedido"),
|
|
createButton("En preparación", "En preparación"),
|
|
createButton("Listo", "Listo"),
|
|
createButton("Entregado", "Entregado"),
|
|
createButton("Deuda", "Deuda"),
|
|
];
|
|
const paidButton = document.createElement("button");
|
|
paidButton.textContent = "Pagado";
|
|
paidButton.className = "btn5";
|
|
paidButton.onclick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// Open Pagos module with pre-filled data
|
|
var precio = SC_priceCalc(JSON.parse(data.Comanda))[0];
|
|
var personaId = data.Persona;
|
|
var comandaId = data._key;
|
|
|
|
// Store prefilled data in sessionStorage for Pagos module
|
|
var sdata = JSON.stringify({
|
|
tipo: "Gasto",
|
|
monto: precio / 100, // Convert cents to euros
|
|
persona: personaId,
|
|
notas:
|
|
"Pago de comanda SuperCafé\n" +
|
|
SC_parse(JSON.parse(data.Comanda)),
|
|
origen: "SuperCafé",
|
|
origen_id: comandaId,
|
|
});
|
|
|
|
// Navigate to datafono
|
|
setUrlHash("pagos,datafono_prefill," + btoa(sdata));
|
|
|
|
return false;
|
|
};
|
|
td.append(data.Fecha);
|
|
td.append(document.createElement("br"));
|
|
buttons.forEach((button) => {
|
|
td.appendChild(button);
|
|
td.appendChild(document.createElement("br"));
|
|
});
|
|
td.appendChild(paidButton);
|
|
new_tr.appendChild(td);
|
|
break;
|
|
}
|
|
case "persona": {
|
|
let persona =
|
|
key.self === true ? data : SC_Personas[data[key.key]] || {};
|
|
const regco = stringToColour((persona.Region || "?").toLowerCase());
|
|
const tdPersona = document.createElement("td");
|
|
tdPersona.style.textAlign = "center";
|
|
tdPersona.style.fontSize = "20px";
|
|
tdPersona.style.backgroundColor = regco;
|
|
tdPersona.style.color = colorIsDarkAdvanced(regco);
|
|
const regionSpan = document.createElement("span");
|
|
regionSpan.style.fontSize = "40px";
|
|
regionSpan.style.textTransform = "capitalize";
|
|
regionSpan.textContent = (persona.Region || "?").toLowerCase();
|
|
tdPersona.appendChild(regionSpan);
|
|
tdPersona.appendChild(document.createElement("br"));
|
|
const infoSpan = document.createElement("span");
|
|
infoSpan.style.backgroundColor = "white";
|
|
infoSpan.style.border = "2px solid black";
|
|
infoSpan.style.borderRadius = "5px";
|
|
infoSpan.style.display = "inline-block";
|
|
infoSpan.style.padding = "5px";
|
|
infoSpan.style.color = "black";
|
|
const img = document.createElement("img");
|
|
img.src = persona.Foto || "static/ico/user_generic.png";
|
|
// Prefer attachment 'foto' stored in PouchDB if available
|
|
try {
|
|
const personaId = key.self === true ? (data._key || data._id || data.id) : data[key.key];
|
|
if (personaId) {
|
|
DB.getAttachment('personas', personaId, 'foto').then((durl) => {
|
|
if (durl) img.src = durl;
|
|
}).catch(() => {});
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
img.height = 70;
|
|
infoSpan.appendChild(img);
|
|
infoSpan.appendChild(document.createElement("br"));
|
|
infoSpan.appendChild(document.createTextNode(persona.Nombre || ""));
|
|
infoSpan.appendChild(document.createElement("br"));
|
|
if (parseFloat(persona.Monedero_Balance || "0") != 0) {
|
|
const pointsSpan = document.createElement("span");
|
|
pointsSpan.style.fontSize = "17px";
|
|
pointsSpan.textContent = parseFloat(persona.Monedero_Balance || "0").toPrecision(2) + " €";
|
|
infoSpan.appendChild(pointsSpan);
|
|
}
|
|
tdPersona.appendChild(infoSpan);
|
|
new_tr.appendChild(tdPersona);
|
|
break;
|
|
}
|
|
case "persona-nombre": {
|
|
let persona =
|
|
key.self === true ? data : SC_Personas[data[key.key]] || {};
|
|
const tdPersonaNombre = document.createElement("td");
|
|
tdPersonaNombre.style.textAlign = "center";
|
|
tdPersonaNombre.style.fontSize = "20px";
|
|
tdPersonaNombre.textContent = persona.Nombre || "";
|
|
new_tr.appendChild(tdPersonaNombre);
|
|
break;
|
|
}
|
|
case "attachment-persona": {
|
|
const tdAttachment = document.createElement("td");
|
|
const img = document.createElement("img");
|
|
img.src =
|
|
data[key.key] ||
|
|
"static/ico/user_generic.png";
|
|
img.style.maxHeight = "80px";
|
|
img.style.maxWidth = "80px";
|
|
tdAttachment.appendChild(img);
|
|
new_tr.appendChild(tdAttachment);
|
|
// Prefer attachment 'foto' stored in PouchDB if available
|
|
try {
|
|
const personaId = key.self === true ? (data._key || data._id || data.id) : data[key.key];
|
|
if (personaId) {
|
|
DB.getAttachment('personas', personaId, 'foto').then((durl) => {
|
|
if (durl) img.src = durl;
|
|
}).catch(() => {});
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
new_tr.onclick = (event) => {
|
|
setUrlHash(pageco + "," + data._key);
|
|
};
|
|
fragment.appendChild(new_tr);
|
|
}
|
|
// Replace tbody in one operation
|
|
tablebody_EL.innerHTML = "";
|
|
tablebody_EL.appendChild(fragment);
|
|
}
|
|
// Subscribe to dataset updates using DB.map (PouchDB) when `ref` is a table name string
|
|
if (typeof ref === 'string') {
|
|
DB.map(ref, (data, key) => {
|
|
function add_row(data, key) {
|
|
if (data != null) {
|
|
data["_key"] = key;
|
|
rows[key] = data;
|
|
} else {
|
|
delete rows[key];
|
|
}
|
|
debounce(debounce_load, render, 300, [rows]);
|
|
}
|
|
if (typeof data == "string") {
|
|
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
|
data["_encrypted__"] = wasEncrypted;
|
|
add_row(data, key);
|
|
}, ref, key);
|
|
} else {
|
|
data["_encrypted__"] = false;
|
|
add_row(data, key);
|
|
}
|
|
});
|
|
} else if (ref && typeof ref.map === 'function') {
|
|
// Legacy: try to use ref.map().on if available (for backwards compatibility)
|
|
try {
|
|
ref.map().on((data, key, _msg, _ev) => {
|
|
function add_row(data, key) {
|
|
if (data != null) {
|
|
data["_key"] = key;
|
|
rows[key] = data;
|
|
} else {
|
|
delete rows[key];
|
|
}
|
|
debounce(debounce_load, render, 300, [rows]);
|
|
}
|
|
if (typeof data == "string") {
|
|
TS_decrypt(data, SECRET, (data, wasEncrypted) => {
|
|
data["_encrypted__"] = wasEncrypted;
|
|
add_row(data, key);
|
|
}, undefined, undefined);
|
|
} else {
|
|
data["_encrypted__"] = false;
|
|
add_row(data, key);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.warn('TS_IndexElement: cannot subscribe to ref', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function BuildQR(mid, label) {
|
|
return `
|
|
<span style="border: 2px dashed black; padding: 10px; display: inline-block; background: white; border-radius: 7px; text-align: center; margin: 10px;">
|
|
<b>QR %%TITLE%%</b>
|
|
<br>${toHtml(quickresponse(mid), [6, 6])}<br>
|
|
<small>${label}</small>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
var PAGES = {};
|
|
var PERMS = {
|
|
ADMIN: "Administrador",
|
|
};
|
|
function checkRole(role) {
|
|
var roles = SUB_LOGGED_IN_DETAILS.Roles || "";
|
|
var rolesArr = roles.split(",");
|
|
if (rolesArr.includes("ADMIN") || rolesArr.includes(role) || AC_BYPASS) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
function SetPages() {
|
|
document.getElementById("appendApps2").innerHTML = "";
|
|
Object.keys(PAGES).forEach((key) => {
|
|
if (PAGES[key].Esconder == true) {
|
|
return;
|
|
}
|
|
if (PAGES[key].AccessControl == true) {
|
|
var roles = SUB_LOGGED_IN_DETAILS.Roles || "";
|
|
var rolesArr = roles.split(",");
|
|
if (rolesArr.includes("ADMIN") || rolesArr.includes(key) || AC_BYPASS) {
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
var a = document.createElement("a");
|
|
var img = document.createElement("img");
|
|
var label = document.createElement("div");
|
|
a.className = "ribbon-button";
|
|
a.href = "#" + key;
|
|
label.innerText = PAGES[key].Title;
|
|
label.className = "label";
|
|
img.src = PAGES[key].icon || "static/appico/application_enterprise.png";
|
|
a.append(img, label);
|
|
document.getElementById("appendApps2").append(a);
|
|
});
|
|
var a = document.createElement("a");
|
|
var img = document.createElement("img");
|
|
var label = document.createElement("div");
|
|
a.className = "ribbon-button";
|
|
a.href = "#index,qr";
|
|
label.innerText = "Escanear QR";
|
|
label.className = "label";
|
|
img.src = "static/appico/barcode.png";
|
|
a.append(img, label);
|
|
document.getElementById("appendApps2").append(a);
|
|
}
|
|
var Booted = false;
|
|
var TimeoutBoot = 3; // in loops of 750ms
|
|
var BootLoops = 0;
|
|
|
|
// Get URL host for peer link display
|
|
var couchDatabase = localStorage.getItem("TELESEC_COUCH_DBNAME") || "telesec";
|
|
var couchUrl = localStorage.getItem("TELESEC_COUCH_URL") || null;
|
|
var couchHost = "";
|
|
try {
|
|
var urlObj = new URL(couchUrl);
|
|
couchHost = urlObj.host;
|
|
} catch (e) {
|
|
couchHost = couchUrl;
|
|
}
|
|
if (couchHost) {
|
|
document.getElementById("peerLink").innerText = couchDatabase + "@" + couchHost;
|
|
}
|
|
|
|
function getPeers() {
|
|
const peerListEl = document.getElementById("peerList");
|
|
const pidEl = document.getElementById("peerPID");
|
|
const statusImg = document.getElementById("connectStatus");
|
|
|
|
// Default status based on navigator
|
|
if (window.navigator && window.navigator.onLine === false) {
|
|
if (statusImg) statusImg.src = "static/ico/offline.svg";
|
|
} else {
|
|
if (statusImg) statusImg.src = "static/logo.jpg";
|
|
}
|
|
|
|
// Clear previous list
|
|
if (peerListEl) peerListEl.innerHTML = "";
|
|
|
|
// Show local DB stats if available
|
|
if (window.DB && DB._internal && DB._internal.local) {
|
|
DB._internal.local
|
|
.info()
|
|
.then((info) => {
|
|
if (peerListEl) {
|
|
const li = document.createElement("li");
|
|
li.innerText = `Local DB: ${info.db_name || "telesec"} (docs: ${info.doc_count || 0})`;
|
|
peerListEl.appendChild(li);
|
|
}
|
|
if (pidEl) pidEl.innerText = `DB: ${info.db_name || "telesec"}`;
|
|
})
|
|
.catch(() => {
|
|
if (peerListEl) {
|
|
const li = document.createElement("li");
|
|
li.innerText = "Local DB: unavailable";
|
|
peerListEl.appendChild(li);
|
|
}
|
|
if (pidEl) pidEl.innerText = "DB: local";
|
|
});
|
|
} else {
|
|
if (pidEl) pidEl.innerText = "DB: none";
|
|
}
|
|
}
|
|
|
|
getPeers();
|
|
setInterval(() => {
|
|
getPeers();
|
|
}, PeerConnectionInterval);
|
|
|
|
var BootIntervalID = setInterval(() => {
|
|
BootLoops += 1;
|
|
getPeers();
|
|
|
|
const isOnline = window.navigator ? window.navigator.onLine !== false : true;
|
|
|
|
// Check if local DB is initialized and responsive
|
|
const checkLocalDB = () => {
|
|
if (window.DB && DB._internal && DB._internal.local) {
|
|
return DB._internal.local.info().then(() => true).catch(() => false);
|
|
}
|
|
return Promise.resolve(false);
|
|
};
|
|
|
|
checkLocalDB().then((dbReady) => {
|
|
// If offline, or DB ready, or we've waited long enough, proceed to boot the UI
|
|
if ((dbReady || !isOnline || BootLoops >= TimeoutBoot) && !Booted) {
|
|
Booted = true;
|
|
document.getElementById("loading").style.display = "none";
|
|
|
|
if (!isOnline) {
|
|
toastr.error(
|
|
"Sin conexión! Los cambios se sincronizarán cuando vuelvas a estar en línea."
|
|
);
|
|
}
|
|
|
|
if (!SUB_LOGGED_IN) {
|
|
if (AC_BYPASS) {
|
|
// Auto-create or load a bypass persona and log in automatically
|
|
const bypassId = localStorage.getItem('TELESEC_BYPASS_ID') || 'bypass-admin';
|
|
if (window.DB && DB.get) {
|
|
DB.get('personas', bypassId).then((data) => {
|
|
function finish(pdata, id) {
|
|
SUB_LOGGED_IN_ID = id || bypassId;
|
|
SUB_LOGGED_IN_DETAILS = pdata || {};
|
|
SUB_LOGGED_IN = true;
|
|
localStorage.setItem('TELESEC_BYPASS_ID', SUB_LOGGED_IN_ID);
|
|
SetPages();
|
|
open_page(location.hash.replace("#", ""));
|
|
}
|
|
if (!data) {
|
|
const persona = { Nombre: 'Admin (bypass)', Roles: 'ADMIN,' };
|
|
DB.put('personas', bypassId, persona).then(() => finish(persona, bypassId)).catch((e) => { console.warn('AC_BYPASS create error', e); open_page('login'); });
|
|
} else {
|
|
if (typeof data === 'string') {
|
|
TS_decrypt(data, SECRET, (pdata) => finish(pdata, bypassId), 'personas', bypassId);
|
|
} else {
|
|
finish(data, bypassId);
|
|
}
|
|
}
|
|
}).catch((e) => {
|
|
console.warn('AC_BYPASS persona check error', e);
|
|
open_page('login');
|
|
});
|
|
} else {
|
|
// DB not ready, fallback to login page
|
|
open_page('login');
|
|
}
|
|
} else {
|
|
open_page("login");
|
|
}
|
|
} else {
|
|
SetPages();
|
|
open_page(location.hash.replace("#", ""));
|
|
}
|
|
clearInterval(BootIntervalID);
|
|
}
|
|
});
|
|
}, 750);
|
|
|
|
const tabs = document.querySelectorAll(".ribbon-tab");
|
|
const detailTabs = {
|
|
modulos: document.getElementById("tab-modulos"),
|
|
buscar: document.getElementById("tab-buscar"),
|
|
};
|
|
|
|
tabs.forEach((tab) => {
|
|
tab.addEventListener("click", () => {
|
|
const selected = tab.getAttribute("data-tab");
|
|
|
|
// Toggle details
|
|
for (const [key, detailsEl] of Object.entries(detailTabs)) {
|
|
if (key === selected) {
|
|
detailsEl.setAttribute("open", "");
|
|
} else {
|
|
detailsEl.removeAttribute("open");
|
|
}
|
|
}
|
|
|
|
// Toggle tab active class
|
|
tabs.forEach((t) => t.classList.remove("active"));
|
|
tab.classList.add("active");
|
|
});
|
|
});
|
|
|
|
// Global Search Functionality
|
|
function GlobalSearch() {
|
|
const searchData = {};
|
|
const allSearchableModules = [
|
|
{
|
|
role: "personas",
|
|
key: "personas",
|
|
title: "Personas",
|
|
icon: "static/appico/File_Person.svg",
|
|
fields: ["Nombre", "Region", "Notas", "email"],
|
|
},
|
|
{
|
|
role: "materiales",
|
|
key: "materiales",
|
|
title: "Materiales",
|
|
icon: "static/appico/Database.svg",
|
|
fields: ["Nombre", "Referencia", "Ubicacion", "Notas"],
|
|
},
|
|
{
|
|
role: "supercafe",
|
|
key: "supercafe",
|
|
title: "SuperCafé",
|
|
icon: "static/appico/Coffee.svg",
|
|
fields: ["Persona", "Comanda", "Estado"],
|
|
},
|
|
{
|
|
role: "comedor",
|
|
key: "comedor",
|
|
title: "Comedor",
|
|
icon: "static/appico/Meal.svg",
|
|
fields: ["Fecha", "Platos"],
|
|
},
|
|
{
|
|
role: "notas",
|
|
key: "notas",
|
|
title: "Notas",
|
|
icon: "static/appico/Notepad.svg",
|
|
fields: ["Asunto", "Contenido", "Autor"],
|
|
},
|
|
{
|
|
role: "notificaciones",
|
|
key: "notificaciones",
|
|
title: "Avisos",
|
|
icon: "static/appico/Alert_Warning.svg",
|
|
fields: ["Asunto", "Mensaje", "Origen", "Destino"],
|
|
},
|
|
{
|
|
role: "aulas",
|
|
key: "aulas_solicitudes",
|
|
title: "Solicitudes de Aulas",
|
|
icon: "static/appico/Classroom.svg",
|
|
fields: ["Asunto", "Contenido", "Solicitante"],
|
|
},
|
|
{
|
|
role: "aulas",
|
|
key: "aulas_informes",
|
|
title: "Informes de Aulas",
|
|
icon: "static/appico/Newspaper.svg",
|
|
fields: ["Asunto", "Contenido", "Autor", "Fecha"],
|
|
},
|
|
];
|
|
|
|
// Filter modules based on user permissions
|
|
const searchableModules = allSearchableModules.filter((module) => {
|
|
return checkRole(module.role);
|
|
});
|
|
|
|
// Load all data from modules
|
|
function loadAllData() {
|
|
searchableModules.forEach((module) => {
|
|
searchData[module.key] = {};
|
|
DB.map(module.key, (data, key) => {
|
|
if (!data) return;
|
|
|
|
function processData(processedData) {
|
|
if (processedData && typeof processedData === "object") {
|
|
searchData[module.key][key] = {
|
|
_key: key,
|
|
_module: module.key,
|
|
_title: module.title,
|
|
_icon: module.icon,
|
|
...processedData,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (typeof data === "string") {
|
|
TS_decrypt(data, SECRET, processData);
|
|
} else {
|
|
processData(data);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Perform search across all modules
|
|
function performSearch(searchTerm) {
|
|
if (!searchTerm || searchTerm.length < 2) return [];
|
|
|
|
const results = [];
|
|
const searchLower = searchTerm.toLowerCase();
|
|
|
|
searchableModules.forEach((module) => {
|
|
const moduleData = searchData[module.key] || {};
|
|
|
|
Object.values(moduleData).forEach((item) => {
|
|
if (!item) return;
|
|
|
|
let relevanceScore = 0;
|
|
let matchedFields = [];
|
|
|
|
// Search in key/ID
|
|
if (item._key && item._key.toLowerCase().includes(searchLower)) {
|
|
relevanceScore += 10;
|
|
matchedFields.push("ID");
|
|
}
|
|
|
|
// Search in configured fields
|
|
module.fields.forEach((field) => {
|
|
const value = item[field];
|
|
if (!value) return;
|
|
|
|
let searchValue = "";
|
|
|
|
// Handle special field types
|
|
if (field === "Persona" && SC_Personas[value]) {
|
|
searchValue = SC_Personas[value].Nombre || "";
|
|
} else if (field === "Comanda" && typeof value === "string") {
|
|
try {
|
|
const comandaData = JSON.parse(value);
|
|
searchValue = Object.values(comandaData).join(" ");
|
|
} catch (e) {
|
|
searchValue = value;
|
|
}
|
|
} else {
|
|
searchValue = String(value);
|
|
}
|
|
|
|
if (searchValue.toLowerCase().includes(searchLower)) {
|
|
relevanceScore += field === "Nombre" || field === "Asunto" ? 5 : 2;
|
|
matchedFields.push(field);
|
|
}
|
|
});
|
|
|
|
if (relevanceScore > 0) {
|
|
results.push({
|
|
...item,
|
|
_relevance: relevanceScore,
|
|
_matchedFields: matchedFields,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return results.sort((a, b) => b._relevance - a._relevance);
|
|
}
|
|
|
|
// Render search results
|
|
function renderResults(results, container) {
|
|
if (results.length === 0) {
|
|
container.innerHTML = `
|
|
<fieldset>
|
|
<legend>Sin resultados</legend>
|
|
<div>🚫 No se encontraron resultados</div>
|
|
<p>Prueba con otros términos de búsqueda o usa filtros diferentes</p>
|
|
</fieldset>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
|
|
// Group by module
|
|
const groupedResults = {};
|
|
results.forEach((result) => {
|
|
if (!groupedResults[result._module]) {
|
|
groupedResults[result._module] = [];
|
|
}
|
|
groupedResults[result._module].push(result);
|
|
});
|
|
|
|
Object.entries(groupedResults).forEach(([moduleKey, moduleResults]) => {
|
|
const module = searchableModules.find((m) => m.key === moduleKey);
|
|
if (!module) return;
|
|
|
|
html += `
|
|
<fieldset>
|
|
<legend>
|
|
<img src="${module.icon}" height="20"> ${module.title} (${moduleResults.length})
|
|
</legend>
|
|
`;
|
|
|
|
moduleResults.slice(0, 5).forEach((result) => {
|
|
let title = result.Nombre || result.Asunto || result._key;
|
|
let subtitle = "";
|
|
|
|
// Handle comedor specific display
|
|
if (result._module === "comedor") {
|
|
title = result.Fecha
|
|
? `Menú del ${result.Fecha.split("-").reverse().join("/")}`
|
|
: result._key;
|
|
if (result.Platos) {
|
|
subtitle = `🍽️ ${result.Platos.substring(0, 50)}${
|
|
result.Platos.length > 50 ? "..." : ""
|
|
}`;
|
|
}
|
|
} else {
|
|
// Default display for other modules
|
|
if (result.Persona && SC_Personas[result.Persona]) {
|
|
subtitle = `👤 ${SC_Personas[result.Persona].Nombre}`;
|
|
}
|
|
if (result.Fecha) {
|
|
const fecha = result.Fecha.split("-").reverse().join("/");
|
|
subtitle += subtitle ? ` • 📅 ${fecha}` : `📅 ${fecha}`;
|
|
}
|
|
if (result.Region) {
|
|
subtitle += subtitle
|
|
? ` • 🌍 ${result.Region}`
|
|
: `🌍 ${result.Region}`;
|
|
}
|
|
}
|
|
|
|
html += `
|
|
<button onclick="navigateToResult('${moduleKey}', '${
|
|
result._key
|
|
}')" class="button">
|
|
<strong>${title}</strong>
|
|
${subtitle ? `<br><small>${subtitle}</small>` : ""}
|
|
<br><code>📍 ${result._matchedFields.join(", ")}</code>
|
|
</button>
|
|
`;
|
|
});
|
|
|
|
if (moduleResults.length > 5) {
|
|
let moreLink = moduleKey;
|
|
if (moduleKey === "aulas_solicitudes") {
|
|
moreLink = "aulas,solicitudes";
|
|
} else if (moduleKey === "aulas_informes") {
|
|
moreLink = "aulas,informes";
|
|
}
|
|
|
|
html += `
|
|
<hr>
|
|
<button onclick="setUrlHash('${moreLink}')" class="btn8">
|
|
Ver ${moduleResults.length - 5} resultados más en ${module.title}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
html += "</fieldset>";
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
return {
|
|
loadAllData,
|
|
performSearch,
|
|
renderResults,
|
|
getAccessibleModules: () => searchableModules,
|
|
};
|
|
}
|
|
|
|
// Helper function to navigate to search results
|
|
function navigateToResult(moduleKey, resultKey) {
|
|
switch (moduleKey) {
|
|
case "aulas_solicitudes":
|
|
setUrlHash("aulas,solicitudes," + resultKey);
|
|
break;
|
|
case "aulas_informes":
|
|
setUrlHash("aulas,informes," + resultKey);
|
|
break;
|
|
default:
|
|
setUrlHash(moduleKey + "," + resultKey);
|
|
}
|
|
}
|