implemented ui functions for managing products, uploading images and editiing users

This commit is contained in:
2025-03-09 23:09:49 +01:00
parent fa7f3004fa
commit bd43f03507
23 changed files with 888 additions and 83 deletions

View File

@ -19,6 +19,12 @@ let _api = {
headers: new Headers({ 'content-type': 'application/json' })
};
const response = await fetch(_apiConfig.basePath + path, options);
if(response.status == 404) {
return {
count: 0,
result: []
}
}
// Handle the response
if (!response.ok) {
console.error('Failed to fetch:', response.statusText);
@ -190,13 +196,14 @@ function returnTableDataByTableName(tableName, search="", orderBy="asc", sort=""
}
async function getCountByTable(tableName, search="") {
let baseString = tableName + '?count=true';
let baseString = tableName + '';
if (search && search.length > 0) {
baseString += '&search=' + search;
baseString += '?search=' + search;
}
// Stored in `data:count:${tableName}`
let result = await _api.get(baseString);
console.debug('Count result:', result);
result = result.count;
if (typeof result !== 'number') {
_testPageWarn('Count was not a number, was: ' + result);
console.warn('Count was not a number, was: ' + result);
@ -214,6 +221,8 @@ function _testPageFail(reason) {
}
function _testPageWarn(reason) {
console.warn('API Wrapper Test Warning, reason: ' + reason);
return;
document.getElementById('heroStatus').classList.remove('is-success');
document.getElementById('heroStatus').classList.add('is-warning');

View File

@ -28,4 +28,22 @@ flex-wrap: wrap;
#date {
font-size: 50px;
margin-top: -40px;
}
}
/* HTML: <div class="loader"></div> */
.loader {
height: 200px;
aspect-ratio: 2/3;
--c:no-repeat linear-gradient(#fff 0 0);
background: var(--c), var(--c), var(--c), var(--c);
background-size: 50% 33.4%;
animation: l8 1.5s infinite linear;
}
@keyframes l8 {
0%,
5% {background-position:0 25%,100% 25%,0 75%,100% 75%}
33% {background-position:0 50%,100% 0,0 100%,100% 50%}
66% {background-position:0 50%,0 0,100% 100%,100% 50%}
95%,
100% {background-position:0 75%,0 25%,100% 75%,100% 25%}
}

View File

@ -1,22 +1,26 @@
// Image Handler
const baseUrl = "https://api.unsplash.com/photos/random?client_id=[KEY]&orientation=landscape&topics=nature";
const apiKey = "tYOt7Jo94U7dunVcP5gt-kDKDMjWFOGQNsHuhLDLV8k"; // Take from config
const fullUrl = baseUrl.replace("[KEY]", apiKey);
const baseUrl = 'https://api.unsplash.com/photos/random?client_id=[KEY]&orientation=landscape&topics=nature';
const apiKey = 'tYOt7Jo94U7dunVcP5gt-kDKDMjWFOGQNsHuhLDLV8k'; // Take from config
const fullUrl = baseUrl.replace('[KEY]', apiKey);
const showModeImage = "/static/media/showModeLockscreen.jpg"
const showModeImage = '/static/media/showModeLockscreen.jpg';
let credits = document.getElementById("credits");
let credits = document.getElementById('credits');
let currentImageHandle;
document.body.addEventListener('click', () => {
window.location.href = '/user_select';
});
// Lock screen or show mode
let screenState = "lock";
let screenState = 'lock';
function handleImage() {
if(screenState === "lock") {
fetch("https://staging.thegreydiamond.de/projects/photoPortfolio/api/getRand.php?uuid=01919dec-b2cd-7adc-8ca2-a071d1169cbc&unsplash=true")
.then(response => response.json())
.then(data => {
if (screenState === 'lock') {
fetch('https://staging.thegreydiamond.de/projects/photoPortfolio/api/getRand.php?uuid=01919dec-b2cd-7adc-8ca2-a071d1169cbc&unsplash=true&orientation=landscape')
.then((response) => response.json())
.then((data) => {
// data = {
// urls: {
// regular: "https://imageproxy.thegreydiamond.de/ra5iqxlyve6HpjNvC1tzG50a14oIOgiWP95CxIvbBC8/sm:1/kcr:1/aHR0cHM6Ly9zdGFn/aW5nLnRoZWdyZXlk/aWFtb25kLmRlL3By/b2plY3RzL3Bob3Rv/UG9ydGZvbGlvL2Rl/bW9IaVJlcy9QMTE5/MDgzMC1zY2hpbGQu/anBn.webp"
@ -28,29 +32,29 @@ function handleImage() {
// }
// }
// }
if(!currentImageHandle) {
if (!currentImageHandle) {
// Create a page filling div which contains the image
currentImageHandle = document.createElement("div");
currentImageHandle.style.position = "absolute";
currentImageHandle.style.top = "0";
currentImageHandle.style.left = "0";
currentImageHandle.style.width = "100%";
currentImageHandle.style.height = "100%";
currentImageHandle = document.createElement('div');
currentImageHandle.style.position = 'absolute';
currentImageHandle.style.top = '0';
currentImageHandle.style.left = '0';
currentImageHandle.style.width = '100%';
currentImageHandle.style.height = '100%';
currentImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
currentImageHandle.style.backgroundSize = "cover";
currentImageHandle.style.backgroundSize = 'cover';
currentImageHandle.style.opacity = 1;
} else {
// Create a new div behind the current one and delete the old one when the new one is loaded
let newImageHandle = document.createElement("div");
newImageHandle.style.position = "absolute";
newImageHandle.style.top = "0";
newImageHandle.style.left = "0";
newImageHandle.style.width = "100%";
newImageHandle.style.height = "100%";
let newImageHandle = document.createElement('div');
newImageHandle.style.position = 'absolute';
newImageHandle.style.top = '0';
newImageHandle.style.left = '0';
newImageHandle.style.width = '100%';
newImageHandle.style.height = '100%';
newImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
newImageHandle.style.backgroundSize = "cover";
newImageHandle.style.backgroundSize = 'cover';
newImageHandle.style.opacity = 1;
newImageHandle.style.transition = "1s";
newImageHandle.style.transition = '1s';
newImageHandle.style.zIndex = 19999;
document.body.appendChild(newImageHandle);
@ -61,33 +65,31 @@ function handleImage() {
currentImageHandle = newImageHandle;
}, 1000);
// Set the credits
credits.innerHTML = `Photo by <a href="${data.user.links.html}" target="_blank">${data.user.name}</a> on <a href="https://unsplash.com" target="_blank">Unsplash</a>`;
credits.style.zIndex = 300000;
}
})
.catch(error => {
console.error("Error fetching image: ", error);
.catch((error) => {
console.error('Error fetching image: ', error);
});
} else {
if(currentImageHandle) {
if (currentImageHandle) {
// Check if the image is already loaded
if(currentImageHandle.style.backgroundImage === `url("${showModeImage}")`) {
if (currentImageHandle.style.backgroundImage === `url("${showModeImage}")`) {
return;
}
// Create a new div behind the current one and delete the old one when the new one is loaded
let newImageHandle = document.createElement("div");
newImageHandle.style.position = "absolute";
newImageHandle.style.top = "0";
newImageHandle.style.left = "0";
newImageHandle.style.width = "100%";
newImageHandle.style.height = "100%";
let newImageHandle = document.createElement('div');
newImageHandle.style.position = 'absolute';
newImageHandle.style.top = '0';
newImageHandle.style.left = '0';
newImageHandle.style.width = '100%';
newImageHandle.style.height = '100%';
newImageHandle.style.backgroundImage = `url(${showModeImage})`;
newImageHandle.style.backgroundSize = "cover";
newImageHandle.style.backgroundSize = 'cover';
newImageHandle.style.opacity = 1;
newImageHandle.style.transition = "1s";
newImageHandle.style.transition = '1s';
document.body.appendChild(newImageHandle);
setTimeout(() => {
@ -107,37 +109,22 @@ function handleTimeAndDate() {
month += 1;
let year = time.getFullYear();
let timeHandle = document.getElementById("time");
let dateHandle = document.getElementById("date");
let timeHandle = document.getElementById('time');
let dateHandle = document.getElementById('date');
timeHandle.innerHTML = `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds()}`;
timeHandle.innerHTML = `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds()}`;
// Datum in format Montag, 22.12.2024
dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? "0" + day : day}.${month < 10 ? "0" + month : month}.${year}`;
dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? '0' + day : day}.${month < 10 ? '0' + month : month}.${year}`;
}
function getDay(day) {
switch(day) {
case 0:
return "Sonntag";
case 1:
return "Montag";
case 2:
return "Dienstag";
case 3:
return "Mittwoch";
case 4:
return "Donnerstag";
case 5:
return "Freitag";
case 6:
return "Samstag";
}
return ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][day];
}
// Set the image handler to run every 10 minutes
setInterval(handleImage, 60 * 1000 * 10);
handleImage();
handleImage()
handleImage();
// Set the time and date handler to run every minute
setInterval(handleTimeAndDate, 500);

View File

@ -4,4 +4,12 @@ body {
hidden {
display: none;
}
.notification-container {
position: fixed;
top: 0;
right: 0;
z-index: 1000;
margin: 20px;
}

View File

@ -26,6 +26,13 @@ var searchFields = document.querySelectorAll('input[data-searchTargetId]');
// Find all modalForms
var modalForms = document.querySelectorAll('form[data-targetTable]');
// Create a floating container for notifications
const notificationContainer = document.createElement('div');
notificationContainer.classList.add('notification-container');
document.body.appendChild(notificationContainer);
let notifications = [];
console.info('Processing single values');
console.info(singleValues);
@ -71,8 +78,6 @@ tables.forEach(async (table) => {
});
async function writeSingelton(element) {
const table = element.getAttribute('data-dataSource');
console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element);
@ -181,6 +186,10 @@ modalForms.forEach((modalForm) => {
console.log('Type: ', rule['args']['type']);
break;
}
case 'email': {
field.setAttribute('type', 'email');
break;
}
}
});
if (flags) {
@ -301,7 +310,7 @@ async function refreshTable(table) {
if (searchField) {
const value = searchField.value;
const dbTable = table.getAttribute('data-dataSource');
const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start);
const result = await returnTableDataByTableName(dbTable, value, order, column, take=maxLinesPerPage, skip= start);
const totalResultCount = await getCountByTable(dbTable, value);
paginationPassOnPre['dataLength'] = totalResultCount;
var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
@ -355,6 +364,7 @@ function writeDataToTable(table, data, paginationPassOn) {
if(data == undefined || data == null || data.length == 0) {
return;
}
data = data.result
console.log('Writing data to table: ', table, data);
// Get THEAD and TBODY elements
const thead = table.querySelector('thead');
@ -377,6 +387,9 @@ function writeDataToTable(table, data, paginationPassOn) {
actionFields.push(column);
return;
}
if(column.getAttribute('data-type') == 'hidden') {
return;
}
requiredCols.push(column.getAttribute('data-dataCol'));
});
@ -472,9 +485,30 @@ function writeDataToTable(table, data, paginationPassOn) {
const row = data[resultIndex];
const tr = document.createElement('tr');
requiredCols.forEach((column) => {
// console.log('Column: ', column, ' Index: ', columnIndices[column]);
const td = document.createElement('td');
td.innerText = row[column];
// Grab attribute from header
const header = columns[columnIndices[column]];
if(header.getAttribute('data-dataCol') == "FUNC:INLINE") {
try {
// Call data-ColHandler as a function
const handler = window[header.getAttribute('data-ColHandler')];
const result = handler(row);
row[column] = result;
} catch (e) {
console.error('Error in ColHandler: ', e);
}
}
if(header.getAttribute('data-type') == "bool") {
td.innerHTML = row[column] ? '<i class="bi bi-check"></i>' : '<i class="bi bi-x"></i>';
} else {
td.innerHTML = row[column];
}
tr.appendChild(td);
});
// Add action fields
@ -539,6 +573,9 @@ function writeDataToTable(table, data, paginationPassOn) {
if(field.getAttribute('type') == 'submit') {
return;
}
if(field.getAttribute('data-edit-transfer') == 'disable') {
return;
}
field.value = data[field.getAttribute('name')];
});
form.closest('.modal').classList.add('is-active');
@ -556,9 +593,11 @@ function writeDataToTable(table, data, paginationPassOn) {
if(resp['status'] == 'DELETED') {
refreshTable(table);
updateSingeltonsByTableName(table.getAttribute('data-dataSource'));
createTemporaryNotification('Entry deleted successfully', 'is-success');
} else {
// Show error message
// TODO: Show error message
createTemporaryNotification('Error while deleting entry', 'is-danger');
}
}
break;
@ -605,6 +644,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
const $target = $close.closest('.modal');
if($target.data && $target.data.dissmiss == "false") {
return;
}
$close.addEventListener('click', () => {
closeModal($target);
@ -620,10 +662,20 @@ document.addEventListener('DOMContentLoaded', () => {
});
function createTemporaryNotification(message, type = 'is-success', timeout = 5000) {
const notification = document.createElement('div');
notification.classList.add('notification');
notification.classList.add(type);
notification.innerHTML = message;
notificationContainer.appendChild(notification);
setTimeout(() => {
$(notification).fadeOut(500);
}, timeout);
}
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});

View File

@ -0,0 +1,65 @@
let uploadFileInput = document.getElementById('imgUpload');
let fileName = document.getElementById('fileName');
let imgUploadForm = document.getElementById('imgUploadForm');
function handleImagePresence(row) {
// Check if /api/v1/image?id=row&check returns true
// Needs to be sync
let isThere = false;
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/v1/image?id=${row.id}&check=true`, false);
xhr.send();
if (xhr.status === 200) {
try {
isThere = JSON.parse(xhr.responseText);
} catch (error) {
console.error(error);
isThere = false;
}
}
let pretty = isThere ? '<i class="bi bi-check"></i>' : '<i class="bi bi-x"></i></a>';
const template = `<a href="/api/v1/image?id=${row.id}" target="_blank">${pretty}</a> <i class="bi bi-dot"></i> <button class="btn btn-primary" onclick="uploadImage(${row.id})">Upload</button>`;
return template;
}
function uploadImage(id) {
// Open a file picker
uploadFileInput.click();
// // Open a modal to upload an image
// // Use a form
// const modal = document.getElementById('imageModal');
// modal.style.display = 'block';
imgUploadForm.action = `/api/v1/image?id=${id}`;
}
function silentFormSubmit() {
// Submit the form silently (without reloading the page or redirecting)
// Grab the form and do a POST request (dont forget to prevent default)
const xhr = new XMLHttpRequest();
xhr.open('POST', imgUploadForm.action, true);
xhr.send(new FormData(imgUploadForm));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
//location.reload();
createTemporaryNotification('Bild hochgeladen', 'is-success');
// Close the modal
document.getElementById('imageModal').style.display = "none";
// Empty the input
uploadFileInput.value = '';
}
};
}
uploadFileInput.addEventListener('change', function() {
fileName.innerHTML = this.files[0].name;
silentFormSubmit();
setTimeout(() => {
refreshTableByName('products');
}, 1000);
});

View File

@ -0,0 +1,2 @@
let elm_table_users = document.getElementById('table_users');

4
static/pages/payup.js Normal file
View File

@ -0,0 +1,4 @@
const tableContent = document.querySelector('.table-content');
const tableSum = document.querySelector('.table-sum');
alert("NYI: Endpoint is not yet implemented. This demo ends here.");

View File

@ -2,11 +2,35 @@ console.log('product_select.js loaded');
// Get containers
let mainSelectionDiv = document.getElementById('mainSelect');
let checkoutTable = document.getElementById('selectedProducts');
let sumField = document.getElementById('TableSum');
let toCheckoutButton = document.getElementById('checkout');
let confirmCartButton = document.getElementById('confirmCheckout');
let loadingModal = document.getElementById('loadingModal');
let scannerField = document.getElementById('scannerField');
const baseStruct = document.getElementById("baseStruct");
let globalData;
let shoppingCart = [];
toCheckoutButton.addEventListener('click', finalizeTransaction);
confirmCartButton.addEventListener('click', confirmedCart);
// Get user from url (and cookie)
let userFCookie = getCookie('user');
let userFUrl = new URLSearchParams(window.location.search).get('user');
if(userFCookie != userFUrl) {
createTemporaryNotification('Fehler: User nicht korrekt gesetzt!', 'is-danger');
window.location.href = '/user_select';
}
// On load
document.addEventListener('DOMContentLoaded', async function() {
let data = await returnTableDataByTableName('products');
@ -17,7 +41,7 @@ document.addEventListener('DOMContentLoaded', async function() {
for(let i = 0; i < result.length; i++) {
let product = result[i];
if(product.visible) {
if(product.visible && product.stock > 0) {
let newDiv = baseStruct.cloneNode(true);
newDiv.id = `product_${product.id}`;
newDiv.style.display = 'block';
@ -27,9 +51,186 @@ document.addEventListener('DOMContentLoaded', async function() {
newDiv.querySelector('.product_price').innerText = price + " €";
newDiv.querySelector('.product_ean').innerText = product.gtin;
newDiv.querySelector('.product_image').src = product.image || "https://bulma.io/assets/images/placeholders/1280x960.png";
newDiv.querySelector('.product_image').src = "/api/v1/image?id=" + product.id;
newDiv.querySelector('.product_image').alt = product.name;
newDiv.addEventListener('click', selectProductEvent);
mainSelectionDiv.appendChild(newDiv);
}
}
});
function canIAddProduct(product, shoppingCart) {
let stock = product.stock;
let count = shoppingCart.filter(p => p.id == product.id).length;
return count < stock;
}
function selectProductEvent(e) {
console.log('selectProductEvent', e);
let id = e.currentTarget.id.split('_')[1];
let product = globalData.find(p => p.id == id);
if(!canIAddProduct(product, shoppingCart)) {
createTemporaryNotification('Nicht genug Lagerbestand mehr vorhanden!', 'is-danger');
return;
}
let price = parseFloat(product.price).toFixed(2);
let row = checkoutTable.insertRow();
row.id = `product_${product.id}`;
let cell1 = row.insertCell(0); // Name
let cell2 = row.insertCell(1); // Price
let cell3 = row.insertCell(2); // Actions
shoppingCart.push(product);
cell1.innerText = product.name;
cell2.innerText = price + " €";
let deleteButton = document.createElement('button');
deleteButton.innerHTML = '<i class="bi bi-trash"></i>';
deleteButton.onclick = deleteProductEvent;
deleteButton.className = 'button is-danger';
deleteButton.style.color = 'white';
cell3.appendChild(deleteButton);
sumField.innerText = calculateSum(shoppingCart);
}
function calculateSum(cart) {
let sum = 0;
for(let i = 0; i < cart.length; i++) {
sum += parseFloat(cart[i].price);
}
return sum.toFixed(2) + " €";
}
function deleteProductEvent(e) {
let row = e.target.parentElement.parentElement;
// Check if icon was clicked instead of button
if(row.tagName != 'TR') {
row = e.target.parentElement.parentElement.parentElement;
}
let id = row.id.split('_')[1];
let product = shoppingCart.find(p => p.id == id);
let index = shoppingCart.indexOf(product);
shoppingCart.splice(index, 1);
row.remove();
sumField.innerText = calculateSum(shoppingCart);
}
function finalizeTransaction() {
if(shoppingCart.length == 0) {
return;
}
// Show confirmation dialog (id-> checkoutModal)
let modal = document.getElementById('checkoutModal');
modal.classList.add('is-active');
let modalContent = document.getElementById('modalContent');
// Grab table in modal
let modalTable = document.getElementById('selectedProductsModal');
modalTable.innerHTML = "";
for(let i = 0; i < shoppingCart.length; i++) {
let product = shoppingCart[i];
let row = modalTable.insertRow();
let cell1 = row.insertCell(0); // Name
let cell2 = row.insertCell(1); // Price
cell1.innerText = product.name;
cell2.innerText = parseFloat(product.price).toFixed(2) + " €";
}
let modalSum = document.getElementById('ModalSum');
modalSum.innerText = calculateSum(shoppingCart);
}
function confirmedCart() {
// Close modal
let modal = document.getElementById('checkoutModal');
modal.classList.remove('is-active');
// Show loading modal
loadingModal.classList.add('is-active');
// Send data to server
// alert('NYI: Send data to server. This demo ends here.');
let listOfIds = shoppingCart.map(p => p.id);
let data = {
products: listOfIds,
user_id: getCookie('user')
};
// Send data to server
fetch('/api/v1/transaction', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(async (response) => {
let json = await response.json();
if(response.ok) {
createTemporaryNotification('<i class="bi bi-check-lg"></i> Erfolgreich abgeschlossen', 'is-success');
setTimeout(() => {
window.location.href = '/user_select';
}, 1000);
} else {
createTemporaryNotification('Fehler: ' + json.error, 'is-danger');
}
loadingModal.classList.remove('is-active');
}).catch((error) => {
createTemporaryNotification('Fehler: ' + error, 'is-danger');
loadingModal.classList.remove('is-active');
});
}
function getCookie(name) {
let value = "; " + document.cookie;
let parts = value.split("; " + name + "=");
if(parts.length == 2) {
return parts.pop().split(";").shift();
}
}
// Handle barcode scanner
// Force the cursor to the scanner field
scannerField.focus();
// Do so in an interval
setInterval(() => {
scannerField.focus();
}, 1000);
// Make it tiny
scannerField.style.fontSize = '1px';
scannerField.style.height = '1px';
scannerField.style.width = '1px';
scannerField.style.opacity = '0';
scannerField.style.position = 'relative';
// Handle barcode scanner input
scannerField.addEventListener('keydown', async function(event) {
if(event.key != 'Enter') {
return;
}
let barcode = scannerField.value;
console.log('Barcode scanned:', barcode);
scannerField.value = "";
// createTemporaryNotification(`Barcode ${barcode} gescannt`, 'is-link');
let product = globalData.find(p => p.gtin == barcode);
if(product) {
let event = new Event('click');
createTemporaryNotification(`<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} gefunden`, 'is-success');
document.getElementById(`product_${product.id}`).dispatchEvent(event);
} else {
createTemporaryNotification( `<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} nicht gefunden`, 'is-danger');
}
});
// Make sure text fields is always centerd vertically
window.addEventListener('scroll', function(event) {
scannerField.y = document.documentElement.scrollTop + 20;
scannerField.style.top = document.documentElement.scrollTop + 20 + "px";
});

View File

@ -100,12 +100,15 @@ function validatePin() {
if(response) {
console.log("Pin is correct");
pinPadModal.classList.remove('is-active');
// Write a cookie
document.cookie = `user=${userId}`;
document.cookie = `name=${currentUser.name}`;
window.location.href = `/product_select?user=${userId}`;
} else {
console.log("Pin is incorrect");
pinValue = "";
updatePinFields();
pinError.classList.remove('is-hidden');
createTemporaryNotification('Fehlerhafte PIN Eingabe!', 'is-danger');
}
});
}