35604-vm/assets/js/main.js
2026-01-26 21:24:18 +00:00

980 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const app = {
dom: {},
state: {
recipes: [],
confirmedRecipeProducts: [],
checkedItems: [],
additionalProducts: [],
user: null
},
api: {
async checkAuth() {
try {
const response = await fetch('api/check_auth.php');
const data = await response.json();
if (data.success && data.logged_in) {
app.state.user = data.user;
if (data.user.shopping_list) {
app.state.checkedItems = data.user.shopping_list.checkedItems || [];
app.state.additionalProducts = data.user.shopping_list.additionalProducts || [];
app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || [];
}
} else {
app.state.user = null;
}
} catch (error) {
console.error('Auth check failed:', error);
}
},
async saveShoppingList() {
if (!app.state.user) return;
try {
await fetch('api/save_shopping_list.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
shopping_list: {
checkedItems: app.state.checkedItems,
additionalProducts: app.state.additionalProducts,
confirmedRecipeProducts: app.state.confirmedRecipeProducts
}
})
});
} catch (error) {
console.error('Failed to save shopping list:', error);
}
},
async login(email, password) {
try {
const response = await fetch('api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
await app.api.checkAuth();
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
return { success: true };
}
return data;
} catch (error) {
return { success: false, error: error.message };
}
},
async register(email, password) {
try {
const response = await fetch('api/register.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
await app.api.checkAuth();
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
return { success: true };
}
return data;
} catch (error) {
return { success: false, error: error.message };
}
},
async logout() {
try {
const response = await fetch('api/logout.php');
const data = await response.json();
if (data.success) {
app.state.user = null;
app.state.recipes = [];
app.state.checkedItems = [];
app.state.additionalProducts = [];
app.state.confirmedRecipeProducts = [];
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
}
} catch (error) {
console.error('Logout failed:', error);
}
},
async getRecipes() {
try {
const response = await fetch('api/get_recipes.php');
const data = await response.json();
if (data.success) {
app.state.recipes = data.recipes;
} else {
console.error('Failed to load recipes:', data.error);
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Ошибка загрузки рецептов.</p></div>';
}
} catch (error) {
console.error('Error:', error);
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Не удалось подключиться к серверу.</p></div>';
}
},
async saveRecipe(formData) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
body: formData
});
return await response.json();
} catch (error) {
alert('Error: ' + error.message);
return { success: false, error: error.message };
}
},
async deleteRecipe(id) {
try {
const response = await fetch('api/delete_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id })
});
const data = await response.json();
if (data.success) {
await app.api.getRecipes();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
} else {
alert('Failed to delete recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
},
ui: {
renderRecipeCards(recipes) {
app.dom.recipeCardsContainer.innerHTML = '';
if (!recipes || recipes.length === 0) {
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-muted">Ваши сохраненные рецепты появятся здесь.</p></div>';
return;
}
recipes.forEach((recipe, index) => {
const cardCol = document.createElement('div');
cardCol.className = 'col-12 mb-3 recipe-card-enter';
cardCol.setAttribute('data-id', recipe.id);
cardCol.style.animationDelay = `${index * 0.1}s`;
const card = document.createElement('div');
card.className = 'card h-100';
if (recipe.image_url) {
const img = document.createElement('img');
img.src = recipe.image_url;
img.className = 'card-img-top';
img.alt = recipe.name;
card.appendChild(img);
}
const cardBody = document.createElement('div');
cardBody.className = 'card-body d-flex flex-column';
const titleWrapper = document.createElement('div');
titleWrapper.className = 'd-flex justify-content-between align-items-start mb-2 gap-2';
const title = document.createElement('h5');
title.className = 'card-title mb-0';
title.textContent = recipe.name;
titleWrapper.appendChild(title);
if (recipe.category) {
const categoryLabel = document.createElement('div');
categoryLabel.className = 'recipe-category-label';
categoryLabel.textContent = recipe.category;
titleWrapper.appendChild(categoryLabel);
}
const text = document.createElement('p');
text.className = 'card-text text-muted';
text.textContent = `${recipe.ingredients.length} ингредиентов`;
const buttonGroup = document.createElement('div');
buttonGroup.className = 'mt-auto pt-2';
buttonGroup.innerHTML = `
<button class="btn btn-light btn-sm view-recipe"><i class="bi bi-eye"></i> Просмотр</button>
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Изменить</button>
<button class="btn btn-danger btn-sm delete-recipe" title="Удалить"><i class="bi bi-trash"></i></button>
`;
cardBody.appendChild(titleWrapper);
cardBody.appendChild(text);
cardBody.appendChild(buttonGroup);
card.appendChild(cardBody);
cardCol.appendChild(card);
app.dom.recipeCardsContainer.appendChild(cardCol);
});
},
updateShoppingList() {
const combinedIngredients = new Map();
// 1. Process recipe ingredients and calculate total based on per-recipe inputs
app.state.recipes.forEach(recipe => {
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipe.id}"]`);
if (!card) return;
const guestsInput = card.querySelector('.recipe-guests-input');
const portionsInput = card.querySelector('.recipe-portions-input');
const targetGuests = guestsInput ? parseInt(guestsInput.value, 10) : recipe.guests;
const targetPortions = portionsInput ? parseInt(portionsInput.value, 10) : 1;
if (isNaN(targetGuests) || targetGuests <= 0) return;
const baseGuests = parseInt(recipe.guests, 10) || 1;
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const name = ing.name.trim();
const unit = ing.unit.trim();
if (!name) return;
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
if (!combinedIngredients.has(key)) {
combinedIngredients.set(key, {
name: name,
unit: unit,
recipeQty: 0,
additionalQty: 0,
sources: [],
category: ing.category
});
}
const item = combinedIngredients.get(key);
item.recipeQty += (ing.quantity || 0) * targetGuests * targetPortions;
if (!item.sources.includes(recipe.name)) {
item.sources.push(recipe.name);
}
});
}
});
// 2. Process additional products
if (app.state.additionalProducts) {
app.state.additionalProducts.forEach(prod => {
const name = prod.name.trim();
const unit = prod.unit.trim();
if (!name) return;
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
if (!combinedIngredients.has(key)) {
combinedIngredients.set(key, {
name: name,
unit: unit,
recipeQty: 0,
additionalQty: 0,
sources: [],
category: prod.category
});
}
const item = combinedIngredients.get(key);
item.additionalQty += prod.quantity;
const source = prod.source || 'Дополнительный продукт';
if (!item.sources.includes(source)) {
item.sources.push(source);
}
if (prod.category && !item.category) {
item.category = prod.category;
}
});
}
// 3. Group for display
const groups = {
'Food': { ingredients: [], label: 'Еда' },
'Drinks': { ingredients: [], label: 'Напитки' },
'Cooking and serving': { ingredients: [], label: 'Приготовление и сервировка' },
'Tableware and consumables': { ingredients: [], label: 'Посуда и расходники' }
};
combinedIngredients.forEach((item, key) => {
let groupName = 'Food'; // Default to Food
if (item.category) {
const normalizedCategory = item.category.toLowerCase();
if (groups.hasOwnProperty(item.category)) {
groupName = item.category;
} else if (normalizedCategory === 'drinks' || normalizedCategory === 'drink') {
groupName = 'Drinks';
}
}
groups[groupName].ingredients.push(item);
});
// 4. Render HTML
let html = '';
let totalIngredients = 0;
for (const groupName in groups) {
const group = groups[groupName];
if (group.ingredients.length > 0) {
totalIngredients += group.ingredients.length;
html += `<h4 class="mt-3">${group.label}</h4>`;
html += '<ul class="list-group list-group-flush">';
group.ingredients.forEach((item, index) => {
const totalQty = item.recipeQty + item.additionalQty;
if (totalQty <= 0) return;
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
const uniqueId = `shopping-item-${groupName.replace(/\s/g, '-')}-${index}`;
const tooltipText = item.sources.join(', ');
const itemKey = `${item.name.toLowerCase()}|${item.unit.toLowerCase()}`;
const isChecked = app.state.checkedItems.includes(itemKey);
html += `<li class="list-group-item d-flex justify-content-between align-items-center ${isChecked ? 'checked' : ''}">
<div class="form-check d-flex align-items-center">
<input class="form-check-input" type="checkbox" id="${uniqueId}" data-key="${itemKey}" ${isChecked ? 'checked' : ''}>
<label class="form-check-label ms-2" for="${uniqueId}">
${item.name}
</label>
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title="${tooltipText}"></i>
</div>`;
html += `<div>
<button class="btn btn-quantity-modifier btn-quantity-minus decrement-item" data-key="${itemKey}">-</button>
<span class="badge bg-custom-green rounded-pill mx-2">${quantityStr} ${item.unit}</span>
<button class="btn btn-quantity-modifier btn-quantity-plus increment-item" data-key="${itemKey}">+</button>
</div>`;
html += `</li>`;
});
html += '</ul>';
}
}
if (totalIngredients === 0) {
html += '<div class="text-center text-muted p-4"><p>Ваш список покупок пуст. Добавьте рецепт, и его ингредиенты появятся здесь.</p></div>';
}
app.dom.shoppingListContainer.innerHTML = html;
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
},
addIngredientRow(ingredient = { name: '', quantity: '', unit: 'г' }) {
const row = document.createElement('div');
row.className = 'ingredient-row mb-3';
const units = ['г', 'кг', 'мл', 'л', 'шт', 'уп'];
const unitButtons = units.map(u =>
`<button type="button" class="btn ${ingredient.unit === u ? 'btn-secondary' : 'btn-outline-secondary'} unit-btn">${u}</button>`
).join('');
row.innerHTML = `
<div class="mb-2">
<input type="text" class="form-control" placeholder="Название ингредиента" aria-label="Ingredient Name" value="${ingredient.name}">
</div>
<div class="d-flex align-items-center">
<input type="number" class="form-control me-2" placeholder="Кол-во" aria-label="Quantity" min="0" step="any" value="${ingredient.quantity}" style="width: 100px;">
<div class="btn-group unit-selector me-2" role="group" aria-label="Unit selector">
${unitButtons}
</div>
<button type="button" class="btn btn-danger btn-sm remove-ingredient ms-auto">&times;</button>
</div>
`;
app.dom.ingredientsContainer.appendChild(row);
},
clearForm() {
app.dom.recipeIdInput.value = '';
app.dom.recipeNameInput.value = '';
app.dom.recipeCategoryInput.value = '';
app.dom.recipeImage.value = '';
app.dom.guestCountInput.value = '1';
app.dom.ingredientsContainer.innerHTML = '';
app.ui.addIngredientRow();
app.dom.newRecipeBtn.textContent = 'Сохранить рецепт';
app.dom.cancelEditBtn.style.display = 'none';
document.getElementById('recipe-form-modal-label').textContent = 'Добавить рецепт';
},
populateFormForEdit(recipeId) {
const recipe = app.state.recipes.find(r => r.id == recipeId);
if (!recipe) return;
app.dom.recipeIdInput.value = recipe.id;
app.dom.recipeNameInput.value = recipe.name;
app.dom.recipeCategoryInput.value = recipe.category || '';
app.dom.guestCountInput.value = recipe.guests;
app.dom.ingredientsContainer.innerHTML = '';
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => app.ui.addIngredientRow(ing));
} else {
app.ui.addIngredientRow();
}
app.dom.newRecipeBtn.textContent = 'Обновить рецепт';
app.dom.cancelEditBtn.style.display = 'block';
document.getElementById('recipe-form-modal-label').textContent = 'Изменить рецепт';
app.dom.recipeFormModal.show();
app.dom.recipeNameInput.focus();
},
populateViewModal(recipeId) {
const recipe = app.state.recipes.find(r => r.id == recipeId);
if (!recipe) return;
const modal = document.getElementById('view-recipe-modal');
const img = modal.querySelector('img');
if (img) {
img.remove();
}
if (recipe.image_url) {
const newImg = document.createElement('img');
newImg.src = recipe.image_url;
newImg.className = 'card-img-top mb-3';
newImg.alt = recipe.name;
modal.querySelector('.modal-body').prepend(newImg);
}
document.getElementById('view-recipe-name').textContent = recipe.name;
document.getElementById('view-recipe-category').textContent = recipe.category || 'Без категории';
document.getElementById('view-recipe-guests').textContent = recipe.guests;
const ingredientsList = document.getElementById('view-recipe-ingredients');
ingredientsList.innerHTML = '';
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${ing.name} - ${ing.quantity} ${ing.unit}`;
ingredientsList.appendChild(li);
});
}
const viewRecipeModal = new bootstrap.Modal(modal);
viewRecipeModal.show();
},
getRecipeDataFromForm() {
const recipeName = app.dom.recipeNameInput.value.trim();
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
const category = app.dom.recipeCategoryInput.value;
const ingredients = [];
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
rows.forEach(row => {
const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim();
const qty = parseFloat(row.querySelector('input[placeholder="Qty"]').value);
const activeButton = row.querySelector('.unit-selector .btn-secondary');
const unit = activeButton ? activeButton.textContent.trim() : 'g';
if (name && !isNaN(qty) && qty > 0) {
ingredients.push({ name, quantity: qty, unit });
}
});
if (recipeName && guests > 0 && ingredients.length > 0) {
return { name: recipeName, guests, ingredients, category };
}
return null;
},
loadCheckedItems() {
const stored = localStorage.getItem('checkedItems');
if (stored) {
app.state.checkedItems = JSON.parse(stored);
}
},
saveCheckedItems() {
localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems));
app.api.saveShoppingList();
},
updateAuthNav() {
const nav = document.getElementById('auth-nav');
const guestView = document.getElementById('guest-view');
const appView = document.getElementById('app-view');
if (!nav) return;
if (app.state.user) {
nav.innerHTML = `
<li class="nav-item me-3 d-none d-lg-block">
<span class="text-muted">Добро пожаловать, ${app.state.user.email}</span>
</li>
<li class="nav-item">
<button class="btn btn-outline-primary btn-sm" id="logout-btn">Выйти</button>
</li>
`;
if (guestView) guestView.classList.add('d-none');
if (appView) appView.classList.remove('d-none');
} else {
nav.innerHTML = ''; // Landing page handles login/register
if (guestView) guestView.classList.remove('d-none');
if (appView) appView.classList.add('d-none');
}
}
},
events: {
attachEventListeners() {
// Auth events (Landing Page)
const loginFormLanding = document.getElementById('login-form-landing');
if (loginFormLanding) {
loginFormLanding.addEventListener('submit', async (e) => {
e.preventDefault();
const email = loginFormLanding.querySelector('[name="email"]').value;
const password = loginFormLanding.querySelector('[name="password"]').value;
const result = await app.api.login(email, password);
if (result.success) {
loginFormLanding.reset();
} else {
alert(result.error);
}
});
}
const registerFormLanding = document.getElementById('register-form-landing');
if (registerFormLanding) {
registerFormLanding.addEventListener('submit', async (e) => {
e.preventDefault();
const email = registerFormLanding.querySelector('[name="email"]').value;
const password = registerFormLanding.querySelector('[name="password"]').value;
const result = await app.api.register(email, password);
if (result.success) {
registerFormLanding.reset();
} else {
alert(result.error);
}
});
}
// Toggle login/register on landing
const showRegister = document.getElementById('show-register');
const showLogin = document.getElementById('show-login');
const loginContainer = document.getElementById('login-container');
const registerContainer = document.getElementById('register-container');
if (showRegister && showLogin) {
showRegister.addEventListener('click', (e) => {
e.preventDefault();
loginContainer.classList.add('d-none');
registerContainer.classList.remove('d-none');
});
showLogin.addEventListener('click', (e) => {
e.preventDefault();
registerContainer.classList.add('d-none');
loginContainer.classList.remove('d-none');
});
}
document.getElementById('auth-nav').addEventListener('click', (e) => {
if (e.target.id === 'logout-btn') {
app.api.logout();
}
});
app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
app.dom.aiScanBtn.addEventListener('click', async function() {
const file = app.dom.recipeImage.files[0];
if (!file) {
alert('Пожалуйста, сначала выберите изображение.');
return;
}
app.dom.aiScanBtn.disabled = true;
app.dom.aiScanLoading.classList.remove('d-none');
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('api/scan_recipe.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
const data = result.data;
if (data.name) app.dom.recipeNameInput.value = data.name;
if (data.category) app.dom.recipeCategoryInput.value = data.category;
if (data.guests) app.dom.guestCountInput.value = data.guests;
if (data.ingredients && Array.isArray(data.ingredients)) {
app.dom.ingredientsContainer.innerHTML = '';
data.ingredients.forEach(ing => {
app.ui.addIngredientRow({
name: ing.name || '',
quantity: ing.quantity || '',
unit: ing.unit || 'г'
});
});
}
} else {
alert('Ошибка ИИ сканера: ' + result.error);
}
} catch (error) {
console.error('Error during AI scan:', error);
alert('Произошла ошибка при сканировании ИИ.');
} finally {
app.dom.aiScanBtn.disabled = false;
app.dom.aiScanLoading.classList.add('d-none');
}
});
app.dom.ingredientsContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-ingredient')) {
e.target.closest('.ingredient-row').remove();
} else if (e.target.classList.contains('unit-btn')) {
const group = e.target.closest('.unit-selector');
if (group) {
group.querySelectorAll('.unit-btn').forEach(btn => {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.remove('btn-outline-secondary');
e.target.classList.add('btn-secondary');
}
}
});
app.dom.newRecipeBtn.addEventListener('click', async function() {
const recipeData = app.ui.getRecipeDataFromForm();
if (!recipeData) {
alert('Пожалуйста, заполните название рецепта, категорию, количество гостей и хотя бы один ингредиент перед сохранением.');
return;
}
const formData = new FormData();
formData.append('name', recipeData.name);
formData.append('guests', recipeData.guests);
formData.append('category', recipeData.category);
formData.append('ingredients', JSON.stringify(recipeData.ingredients));
const recipeId = app.dom.recipeIdInput.value;
if (recipeId) {
formData.append('id', recipeId);
}
const imageInput = document.getElementById('recipeImage');
if (imageInput.files[0]) {
formData.append('image', imageInput.files[0]);
}
const data = await app.api.saveRecipe(formData);
if (data.success && data.recipe) {
app.api.getRecipes().then(() => {
const currentCategory = app.dom.categoryFilters.querySelector('.active').dataset.category;
if (currentCategory === 'all') {
app.ui.renderRecipeCards(app.state.recipes);
} else {
const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === currentCategory);
app.ui.renderRecipeCards(filteredRecipes);
}
app.ui.updateShoppingList();
app.ui.clearForm();
app.dom.recipeFormModal.hide();
});
} else {
alert('Не удалось сохранить рецепт: ' + data.error);
}
});
app.dom.recipeCardsContainer.addEventListener('click', function(e) {
const target = e.target.closest('button');
const card = e.target.closest('.col-12[data-id]');
if (!card) return;
const recipeId = card.getAttribute('data-id');
if (target && target.classList.contains('delete-recipe')) {
if (confirm('Вы уверены, что хотите удалить этот рецепт?')) {
app.api.deleteRecipe(recipeId);
}
}
if (target && target.classList.contains('edit-recipe')) {
app.ui.populateFormForEdit(recipeId);
}
if (target && target.classList.contains('view-recipe')) {
app.ui.populateViewModal(recipeId);
}
});
app.dom.recipeCardsContainer.addEventListener('input', function(e) {
if (e.target.classList.contains('recipe-guests-input') || e.target.classList.contains('recipe-portions-input')) {
app.ui.updateShoppingList();
}
});
app.dom.shoppingListContainer.addEventListener('click', function(e) {
if (e.target.matches('.form-check-input')) {
const listItem = e.target.closest('.list-group-item');
if (listItem) {
listItem.classList.toggle('checked', e.target.checked);
const itemKey = e.target.dataset.key;
if (e.target.checked) {
if (!app.state.checkedItems.includes(itemKey)) {
app.state.checkedItems.push(itemKey);
}
} else {
const index = app.state.checkedItems.indexOf(itemKey);
if (index > -1) {
app.state.checkedItems.splice(index, 1);
}
}
app.ui.saveCheckedItems();
}
} else if (e.target.matches('.increment-item')) {
const key = e.target.dataset.key;
const [name, unit] = key.split('|');
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase());
if (!productToModify) {
const properName = name.charAt(0).toUpperCase() + name.slice(1);
productToModify = {
name: properName,
quantity: 0,
unit: unit,
source: 'Дополнительный продукт'
};
app.state.additionalProducts.push(productToModify);
}
productToModify.quantity++;
app.ui.updateShoppingList();
app.api.saveShoppingList();
} else if (e.target.matches('.decrement-item')) {
const key = e.target.dataset.key;
const [name, unit] = key.split('|');
// Find or create the item in additionalProducts to track adjustments
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase());
if (!productToModify) {
const properName = name.charAt(0).toUpperCase() + name.slice(1);
productToModify = {
name: properName,
quantity: 0,
unit: unit,
source: 'Дополнительный продукт'
};
app.state.additionalProducts.push(productToModify);
}
// Check if this ingredient is part of any recipe
let isRecipeIngredient = false;
let recipeNameForModal = '';
for (const recipe of app.state.recipes) {
if (recipe.ingredients.some(ing => ing.name.toLowerCase() === name.toLowerCase() && ing.unit.toLowerCase() === unit.toLowerCase())) {
isRecipeIngredient = true;
recipeNameForModal = recipe.name; // Just need one name for the modal
break;
}
}
// If it's a recipe ingredient and we're about to remove from the recipe's contribution
if (isRecipeIngredient && productToModify.quantity <= 0) {
const confirmationKey = `${name.toLowerCase()}-${unit.toLowerCase()}`;
if (app.state.confirmedRecipeProducts.includes(confirmationKey)) {
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
} else {
document.getElementById('modal-recipe-name').textContent = recipeNameForModal;
document.getElementById('modal-ingredient-name').textContent = productToModify.name;
const confirmModal = new bootstrap.Modal(document.getElementById('confirmRemoveModal'));
confirmModal.show();
document.getElementById('confirm-remove-btn').onclick = () => {
app.state.confirmedRecipeProducts.push(confirmationKey);
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
confirmModal.hide();
};
}
} else if (productToModify.quantity > 0) {
// It's not a recipe ingredient about to be removed, but has been added via '+'
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
}
// If not a recipe ingredient and quantity is 0, do nothing.
}
});
app.dom.cancelEditBtn.addEventListener('click', function() {
app.ui.clearForm();
app.dom.recipeFormModal.hide();
});
app.dom.recipeSearchInput.addEventListener('input', function() {
const searchTerm = app.dom.recipeSearchInput.value.toLowerCase();
const filteredRecipes = app.state.recipes.filter(recipe => {
const recipeName = recipe.name.toLowerCase();
const ingredients = recipe.ingredients.map(ing => ing.name.toLowerCase()).join(' ');
return recipeName.includes(searchTerm) || ingredients.includes(searchTerm);
});
app.ui.renderRecipeCards(filteredRecipes);
});
app.dom.categoryFilters.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
const category = e.target.dataset.category;
app.dom.categoryFilters.querySelectorAll('button').forEach(btn => {
btn.classList.remove('active', 'btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.add('active', 'btn-secondary');
e.target.classList.remove('btn-outline-secondary');
if (category === 'all') {
app.ui.renderRecipeCards(app.state.recipes);
} else {
const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === category);
app.ui.renderRecipeCards(filteredRecipes);
}
}
});
app.dom.printShoppingListBtn.addEventListener('click', function() {
window.print();
});
app.dom.addProductModal._element.addEventListener('click', function(e) {
if (e.target.classList.contains('unit-btn')) {
const group = e.target.closest('.unit-selector');
if (group) {
group.querySelectorAll('.unit-btn').forEach(btn => {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.remove('btn-outline-secondary');
e.target.classList.add('btn-secondary');
}
}
});
app.dom.addProductForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = app.dom.productNameInput.value.trim();
const quantity = parseFloat(app.dom.productQuantityInput.value);
const unitButton = app.dom.addProductModal._element.querySelector('.unit-selector .btn-secondary');
const unit = unitButton ? unitButton.textContent.trim() : 'g';
const category = app.dom.productCategory.value;
if (!name || isNaN(quantity) || quantity <= 0 || !unit) {
alert('Пожалуйста, заполните все поля корректными значениями.');
return;
}
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
const existingProduct = app.state.additionalProducts.find(p => `${p.name.toLowerCase()}|${p.unit.toLowerCase()}` === key);
if (existingProduct) {
existingProduct.quantity += quantity;
} else {
const newProduct = { name, quantity, unit, source: 'Дополнительный продукт', category: category };
app.state.additionalProducts.push(newProduct);
}
app.ui.updateShoppingList();
app.api.saveShoppingList();
// Reset form
app.dom.productNameInput.value = '';
app.dom.productQuantityInput.value = '1';
app.dom.productCategory.value = 'Food';
const unitButtons = app.dom.addProductModal._element.querySelectorAll('.unit-selector .unit-btn');
unitButtons.forEach((btn, index) => {
if (index === 0) {
btn.classList.add('btn-secondary');
btn.classList.remove('btn-outline-secondary');
} else {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
}
});
app.dom.addProductModal.hide();
});
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
if (!app.dom.recipeIdInput.value) {
app.ui.clearForm();
}
});
}
},
init() {
app.dom = {
recipeNameInput: document.getElementById('recipeName'),
guestCountInput: document.getElementById('guestCount'),
portionsPerGuestInput: document.getElementById('portionsPerGuest'),
ingredientsContainer: document.getElementById('ingredients-container'),
addIngredientBtn: document.getElementById('add-ingredient'),
newRecipeBtn: document.getElementById('new-recipe-btn'),
cancelEditBtn: document.getElementById('cancel-edit-btn'),
shoppingListContainer: document.getElementById('shopping-list-container'),
recipeCardsContainer: document.getElementById('recipe-cards-container'),
recipeIdInput: document.getElementById('recipeId'),
recipeCategoryInput: document.getElementById('recipeCategory'),
recipeImage: document.getElementById('recipeImage'),
aiScanBtn: document.getElementById('ai-scan-btn'),
aiScanLoading: document.getElementById('ai-scan-loading'),
recipeSearchInput: document.getElementById('recipe-search'),
categoryFilters: document.getElementById('category-filters'),
addProductBtn: document.getElementById('add-product-btn'),
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal')),
addProductModal: new bootstrap.Modal(document.getElementById('add-product-modal')),
addProductForm: document.getElementById('add-product-form'),
productNameInput: document.getElementById('productName'),
productQuantityInput: document.getElementById('productQuantity'),
productCategoryWrapper: document.getElementById('product-category-wrapper'),
productCategory: document.getElementById('productCategory'),
};
app.ui.loadCheckedItems();
app.events.attachEventListeners();
app.dom.cancelEditBtn.style.display = 'none';
app.ui.addIngredientRow();
app.api.checkAuth().then(() => {
app.ui.updateAuthNav();
app.api.getRecipes().then(() => {
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
});
});
}
};
document.addEventListener('DOMContentLoaded', app.init);