This commit is contained in:
lubiana 2025-06-08 13:58:51 +02:00
parent 837cfb6d43
commit 939840a3ac
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
76 changed files with 6636 additions and 83 deletions

797
public/assets/js/app.js Normal file
View file

@ -0,0 +1,797 @@
// Sparkle effect on mouse move
document.addEventListener('mousemove', function (e) {
const emojis = ['✨', '💖', '🌟', '💅', '🦄', '🎉', '🌈'];
const sparkle = document.createElement('div');
sparkle.className = 'emoji-footprint';
sparkle.textContent = emojis[Math.floor(Math.random() * emojis.length)];
sparkle.style.left = e.pageX + 'px';
sparkle.style.top = e.pageY + 'px';
document.body.appendChild(sparkle);
setTimeout(() => {
sparkle.remove();
}, 1000);
});
// Dashboard, Order Management, Inventory Management, Settings, and Drink Type functionality
document.addEventListener('DOMContentLoaded', function() {
// Stock update form handling
initStockUpdateForms();
// Edit button handling
initEditButtons();
// Order form handling
initOrderForms();
// Order status form handling
initOrderStatusForms();
// Inventory form handling
initInventoryForm();
// Settings form handling
initSettingsForm();
// Drink Type form handling
initDrinkTypeForm();
});
/**
* Initialize stock update forms with AJAX submission and validation
*/
function initStockUpdateForms() {
// Get all stock update forms
const stockForms = document.querySelectorAll('.stock-update-form');
// Add event listener to each form
stockForms.forEach(form => {
// Add validation to quantity input
const quantityInput = form.querySelector('input[name="quantity"]');
if (quantityInput) {
quantityInput.addEventListener('input', function() {
validateQuantityInput(this);
});
}
// Add submit handler
form.addEventListener('submit', function(e) {
e.preventDefault();
// Validate form before submission
if (!validateStockForm(form)) {
return;
}
// Get form data
const formData = new FormData(form);
// Show loading state
const submitButton = form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Updating...';
submitButton.disabled = true;
// Send AJAX request
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
if (data.success) {
// Show success message
showAlert(form.parentNode, 'success', 'Stock updated successfully.');
} else {
// Show error message
showAlert(form.parentNode, 'danger', `Error updating stock: ${data.message}`);
}
})
.catch(error => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
// Show error message
showAlert(form.parentNode, 'danger', `Error: ${error.message}`);
console.error('Error:', error);
});
});
});
}
/**
* Initialize edit buttons
*/
function initEditButtons() {
const editButtons = document.querySelectorAll('.btn-edit-drink-type');
editButtons.forEach(button => {
button.addEventListener('click', function() {
const drinkTypeId = this.dataset.drinkTypeId;
window.location.href = `/drink-types/edit/${drinkTypeId}`;
});
});
}
/**
* Validate a stock update form
* @param {HTMLFormElement} form - The form to validate
* @returns {boolean} - Whether the form is valid
*/
function validateStockForm(form) {
const quantityInput = form.querySelector('input[name="quantity"]');
return validateQuantityInput(quantityInput);
}
/**
* Validate a quantity input
* @param {HTMLInputElement} input - The input to validate
* @returns {boolean} - Whether the input is valid
*/
function validateQuantityInput(input) {
const value = parseInt(input.value);
// Check if value is a number
if (isNaN(value)) {
input.classList.add('is-invalid');
showInputError(input, 'Please enter a valid number');
return false;
}
// Check if value is non-negative
if (value < 0) {
input.classList.add('is-invalid');
showInputError(input, 'Quantity cannot be negative');
return false;
}
// Input is valid
input.classList.remove('is-invalid');
input.classList.add('is-valid');
return true;
}
/**
* Show an error message for an input
* @param {HTMLInputElement} input - The input with the error
* @param {string} message - The error message
*/
function showInputError(input, message) {
// Remove any existing error message
const existingError = input.parentNode.querySelector('.invalid-feedback');
if (existingError) {
existingError.remove();
}
// Create and add new error message
const errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
errorDiv.textContent = message;
input.parentNode.appendChild(errorDiv);
}
/**
* Show an alert message
* @param {HTMLElement} container - The container to add the alert to
* @param {string} type - The type of alert (success, danger, warning, info)
* @param {string} message - The alert message
*/
function showAlert(container, type, message) {
// Remove any existing alerts
const existingAlerts = container.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
// Create and add new alert
const alert = document.createElement('div');
alert.className = `alert alert-${type} alert-dismissible fade show mt-3`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
container.appendChild(alert);
// Remove alert after 3 seconds
setTimeout(() => {
alert.remove();
}, 3000);
}
/**
* Initialize order forms with validation and dynamic item handling
*/
function initOrderForms() {
const orderForm = document.getElementById('orderForm');
if (!orderForm) return;
// Validate all quantity inputs
const quantityInputs = orderForm.querySelectorAll('input[type="number"]');
quantityInputs.forEach(input => {
input.addEventListener('input', function() {
validateQuantityInput(this);
});
});
// Add form submission handler
orderForm.addEventListener('submit', function(e) {
// Validate all inputs before submission
let isValid = true;
quantityInputs.forEach(input => {
if (!validateQuantityInput(input)) {
isValid = false;
}
});
if (!isValid) {
e.preventDefault();
showAlert(orderForm, 'danger', 'Please correct the errors in the form before submitting.');
}
});
// Add "Add Item" button functionality if it exists
const addItemButton = document.getElementById('addOrderItem');
if (addItemButton) {
addItemButton.addEventListener('click', function() {
addOrderItem();
});
}
// Add "Remove Item" button functionality
const removeButtons = orderForm.querySelectorAll('.remove-order-item');
removeButtons.forEach(button => {
button.addEventListener('click', function() {
removeOrderItem(this);
});
});
}
/**
* Add a new order item row to the order form
*/
function addOrderItem() {
const orderItemsTable = document.querySelector('#orderForm table tbody');
const drinkTypeSelect = document.getElementById('drinkTypeSelect');
if (!orderItemsTable || !drinkTypeSelect) return;
// Get the selected drink type
const selectedOption = drinkTypeSelect.options[drinkTypeSelect.selectedIndex];
const drinkTypeId = selectedOption.value;
const drinkTypeName = selectedOption.text;
// Check if this drink type is already in the table
const existingRow = orderItemsTable.querySelector(`input[value="${drinkTypeId}"]`);
if (existingRow) {
showAlert(orderItemsTable.parentNode, 'warning', `${drinkTypeName} is already in the order.`);
return;
}
// Create a new row
const newRow = document.createElement('tr');
const rowCount = orderItemsTable.querySelectorAll('tr').length;
newRow.innerHTML = `
<td>${drinkTypeName}</td>
<td>
<input type="hidden" name="items[${rowCount}][drinkTypeId]" value="${drinkTypeId}">
<input type="number" name="items[${rowCount}][quantity]"
class="form-control form-control-sm order-quantity"
value="1" min="0">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger remove-order-item">Remove</button>
</td>
`;
// Add event listeners to the new row
const quantityInput = newRow.querySelector('input[type="number"]');
quantityInput.addEventListener('input', function() {
validateQuantityInput(this);
});
const removeButton = newRow.querySelector('.remove-order-item');
removeButton.addEventListener('click', function() {
removeOrderItem(this);
});
// Add the row to the table
orderItemsTable.appendChild(newRow);
}
/**
* Remove an order item row from the order form
* @param {HTMLElement} button - The remove button that was clicked
*/
function removeOrderItem(button) {
const row = button.closest('tr');
if (row) {
row.remove();
}
}
/**
* Initialize order status forms with AJAX submission
*/
function initOrderStatusForms() {
const statusForm = document.querySelector('form[action^="/orders/update-status/"]');
if (!statusForm) return;
statusForm.addEventListener('submit', function(e) {
e.preventDefault();
// Get form data
const formData = new FormData(statusForm);
// Show loading state
const submitButton = statusForm.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Updating...';
submitButton.disabled = true;
// Send AJAX request
fetch(statusForm.action, {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
if (data.success) {
// Show success message
showAlert(statusForm.parentNode, 'success', 'Order status updated successfully.');
// Update the status badge
const statusBadge = document.querySelector('.badge');
if (statusBadge) {
// Remove old status classes
statusBadge.classList.remove('bg-primary', 'bg-warning', 'bg-info', 'bg-success');
// Add new status class
const newStatus = formData.get('status');
if (newStatus === 'new') {
statusBadge.classList.add('bg-primary');
} else if (newStatus === 'in_work') {
statusBadge.classList.add('bg-warning');
} else if (newStatus === 'ordered') {
statusBadge.classList.add('bg-info');
} else if (newStatus === 'fulfilled') {
statusBadge.classList.add('bg-success');
}
// Update the text
statusBadge.textContent = newStatus.charAt(0).toUpperCase() + newStatus.slice(1);
}
} else {
// Show error message
showAlert(statusForm.parentNode, 'danger', `Error updating status: ${data.message}`);
}
})
.catch(error => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
// Show error message
showAlert(statusForm.parentNode, 'danger', `Error: ${error.message}`);
console.error('Error:', error);
});
});
}
/**
* Initialize inventory form with validation, highlighting for changed values, and AJAX submission
*/
function initInventoryForm() {
const inventoryForm = document.getElementById('inventoryForm');
if (!inventoryForm) return;
// Get all quantity inputs
const quantityInputs = inventoryForm.querySelectorAll('.inventory-quantity');
// Add event listener to each input for validation and highlighting
quantityInputs.forEach(input => {
// Store original value for comparison
const originalValue = parseInt(input.dataset.originalValue);
// Add input event listener for validation
input.addEventListener('input', function() {
// Validate input
validateQuantityInput(this);
// Highlight changed values
const currentValue = parseInt(this.value);
if (currentValue !== originalValue) {
this.classList.add('bg-warning', 'text-dark');
} else {
this.classList.remove('bg-warning', 'text-dark');
}
});
});
// Add submit handler
inventoryForm.addEventListener('submit', function(e) {
e.preventDefault();
// Validate all inputs before submission
let isValid = true;
quantityInputs.forEach(input => {
if (!validateQuantityInput(input)) {
isValid = false;
}
});
if (!isValid) {
showAlert(inventoryForm, 'danger', 'Please correct the errors in the form before submitting.');
return;
}
// Check if any values have changed
let hasChanges = false;
quantityInputs.forEach(input => {
const originalValue = parseInt(input.dataset.originalValue);
const currentValue = parseInt(input.value);
if (currentValue !== originalValue) {
hasChanges = true;
}
});
if (!hasChanges) {
showAlert(inventoryForm, 'warning', 'No changes detected. Nothing to update.');
return;
}
// Get form data
const formData = new FormData(inventoryForm);
// Add X-Requested-With header to indicate AJAX request
const headers = new Headers({
'X-Requested-With': 'XMLHttpRequest'
});
// Show loading state
const submitButton = inventoryForm.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
submitButton.textContent = 'Updating...';
submitButton.disabled = true;
// Send AJAX request
fetch(inventoryForm.action, {
method: 'POST',
headers: headers,
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
if (data.success) {
// Show success message
showAlert(inventoryForm, 'success', 'Inventory updated successfully.');
// Update original values and remove highlighting
quantityInputs.forEach(input => {
const drinkTypeId = input.previousElementSibling.value;
const updatedItem = data.updatedItems.find(item => item.drinkTypeId == drinkTypeId);
if (updatedItem) {
input.dataset.originalValue = updatedItem.newQuantity;
input.classList.remove('bg-warning', 'text-dark');
}
});
// Show update results
const updateResults = document.getElementById('updateResults');
const updateResultsContent = document.getElementById('updateResultsContent');
if (updateResults && updateResultsContent) {
updateResults.classList.remove('d-none');
let resultsHtml = '<h6>The following items were updated:</h6><ul>';
data.updatedItems.forEach(item => {
resultsHtml += `<li>${item.name}: ${item.oldQuantity}${item.newQuantity}</li>`;
});
resultsHtml += '</ul>';
updateResultsContent.innerHTML = resultsHtml;
}
} else {
// Show error message
let errorMessage = 'Error updating inventory.';
if (data.errors && data.errors.length > 0) {
errorMessage += ' ' + data.errors.join(' ');
}
showAlert(inventoryForm, 'danger', errorMessage);
}
})
.catch(error => {
// Reset button state
submitButton.textContent = originalText;
submitButton.disabled = false;
// Show error message
showAlert(inventoryForm, 'danger', `Error: ${error.message}`);
console.error('Error:', error);
});
});
}
/**
* Initialize settings form with validation and AJAX submission
*/
function initSettingsForm() {
const settingsForm = document.getElementById('settings-form');
if (!settingsForm) return;
// Get all inputs
const textInputs = settingsForm.querySelectorAll('input[type="text"]');
const numberInputs = settingsForm.querySelectorAll('input[type="number"]');
// Add event listeners for validation
textInputs.forEach(input => {
input.addEventListener('input', function() {
validateSettingTextInput(this);
});
});
numberInputs.forEach(input => {
input.addEventListener('input', function() {
validateSettingNumberInput(this);
});
});
// Add submit handler
settingsForm.addEventListener('submit', function(e) {
// Validate all inputs before submission
let isValid = true;
textInputs.forEach(input => {
if (!validateSettingTextInput(input)) {
isValid = false;
}
});
numberInputs.forEach(input => {
if (!validateSettingNumberInput(input)) {
isValid = false;
}
});
if (!isValid) {
e.preventDefault();
showAlert(settingsForm, 'danger', 'Please correct the errors in the form before submitting.');
}
});
// Reset to defaults button
const resetButton = document.getElementById('reset-defaults');
if (resetButton) {
resetButton.addEventListener('click', function() {
if (confirm('Are you sure you want to reset all settings to their default values?')) {
window.location.href = '/settings/reset';
}
});
}
}
/**
* Validate a text input for settings
* @param {HTMLInputElement} input - The input to validate
* @returns {boolean} - Whether the input is valid
*/
function validateSettingTextInput(input) {
// Check if value is empty
if (input.value.trim() === '') {
input.classList.add('is-invalid');
showInputError(input, 'This field cannot be empty');
return false;
}
// Input is valid
input.classList.remove('is-invalid');
input.classList.add('is-valid');
return true;
}
/**
* Validate a number input for settings
* @param {HTMLInputElement} input - The input to validate
* @returns {boolean} - Whether the input is valid
*/
function validateSettingNumberInput(input) {
const value = parseInt(input.value);
// Check if value is a number
if (isNaN(value)) {
input.classList.add('is-invalid');
showInputError(input, 'Please enter a valid number');
return false;
}
// Check if value is positive
if (value < 1) {
input.classList.add('is-invalid');
showInputError(input, 'Please enter a positive number');
return false;
}
// Input is valid
input.classList.remove('is-invalid');
input.classList.add('is-valid');
return true;
}
/**
* Initialize drink type form with validation and AJAX name uniqueness check
*/
function initDrinkTypeForm() {
const drinkTypeForm = document.getElementById('drink-type-form');
if (!drinkTypeForm) return;
// Get form inputs
const nameInput = drinkTypeForm.querySelector('input[name="name"]');
const descriptionInput = drinkTypeForm.querySelector('textarea[name="description"]');
const desiredStockInput = drinkTypeForm.querySelector('input[name="desired_stock"]');
// Store original name for edit mode
const originalName = nameInput ? nameInput.value : '';
// Add validation for name input
if (nameInput) {
nameInput.addEventListener('input', function() {
validateDrinkTypeName(this, originalName);
});
// Check name uniqueness on blur
nameInput.addEventListener('blur', function() {
checkDrinkTypeNameUniqueness(this, originalName);
});
}
// Add validation for desired stock input
if (desiredStockInput) {
desiredStockInput.addEventListener('input', function() {
validateDesiredStock(this);
});
}
// Add form submission handler
drinkTypeForm.addEventListener('submit', function(e) {
let isValid = true;
// Validate name
if (nameInput && !validateDrinkTypeName(nameInput, originalName)) {
isValid = false;
}
// Validate desired stock
if (desiredStockInput && !validateDesiredStock(desiredStockInput)) {
isValid = false;
}
if (!isValid) {
e.preventDefault();
showAlert(drinkTypeForm, 'danger', 'Please correct the errors in the form before submitting.');
}
});
}
/**
* Validate a drink type name
* @param {HTMLInputElement} input - The input to validate
* @param {string} originalName - The original name (for edit mode)
* @returns {boolean} - Whether the input is valid
*/
function validateDrinkTypeName(input, originalName) {
const value = input.value.trim();
// Check if value is empty
if (value === '') {
input.classList.add('is-invalid');
showInputError(input, 'Name is required');
return false;
}
// Check if value is too long
if (value.length > 255) {
input.classList.add('is-invalid');
showInputError(input, 'Name cannot be longer than 255 characters');
return false;
}
// Input is valid
input.classList.remove('is-invalid');
input.classList.add('is-valid');
return true;
}
/**
* Check if a drink type name is unique
* @param {HTMLInputElement} input - The input to check
* @param {string} originalName - The original name (for edit mode)
*/
function checkDrinkTypeNameUniqueness(input, originalName) {
const value = input.value.trim();
// Skip check if name hasn't changed or is empty
if (value === '' || value === originalName) {
return;
}
// Show loading state
input.classList.add('is-loading');
// Check if name already exists
fetch(`/api/drink-types/check-name?name=${encodeURIComponent(value)}`)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Remove loading state
input.classList.remove('is-loading');
if (data.exists) {
input.classList.add('is-invalid');
showInputError(input, 'A drink type with this name already exists');
}
})
.catch(error => {
// Remove loading state
input.classList.remove('is-loading');
console.error('Error checking name uniqueness:', error);
});
}
/**
* Validate a desired stock input
* @param {HTMLInputElement} input - The input to validate
* @returns {boolean} - Whether the input is valid
*/
function validateDesiredStock(input) {
const value = parseInt(input.value);
// Check if value is a number
if (isNaN(value)) {
input.classList.add('is-invalid');
showInputError(input, 'Desired stock must be a number');
return false;
}
// Check if value is non-negative
if (value < 0) {
input.classList.add('is-invalid');
showInputError(input, 'Desired stock cannot be negative');
return false;
}
// Input is valid
input.classList.remove('is-invalid');
input.classList.add('is-valid');
return true;
}